Repository: juliansngr/mon-ey
Pull request #30: Feature/filter transactions
Author: login: niqeff
Created: 2025-04-07T10:17:17Z | Merged: 2025-04-08T13:40:18Z

Please review this US. Since there is no navigation available yet, please add "/analytics" to URL for reaching the newly added page.


Diff:
diff --git a/components/TransactionFilters/index.js b/components/TransactionFilters/index.js
new file mode 100644
index 0000000..014ae5a
--- /dev/null
+++ b/components/TransactionFilters/index.js
@@ -0,0 +1,89 @@
+import styled from "styled-components";
+import categories from "@/db/categories.json";
+import { useModalContext } from "@/utils/ModalContext/ModalContext";
+import { useTransactionsContext } from "@/utils/TransactionsContext/TransactionsContext";
+import dayjs from "dayjs";
+
+export default function TransactionFilters({
+  filterType,
+  getTransactionsFiltered,
+}) {
+  const { handleModalClose } = useModalContext();
+  const { data, mutate } = useTransactionsContext();
+
+  function handleFilterSubmit(event) {
+    event.preventDefault();
+
+    const formData = new FormData(event.target);
+    const filterData = Object.fromEntries(formData);
+
+    getTransactionsFiltered({
+      allTransactions: [...data],
+      filterCriterium: filterData.filterType,
+      filterPattern: filterData[filterData.filterType],
+    });
+    mutate();
+    handleModalClose();
+  }
+
+  return (
+    <>
+      <StyledFilterForm onSubmit={handleFilterSubmit}>
+        <input type="hidden" name="filterType" value={filterType} />
+        {filterType === "category" && (
+          <StyledFilterSelect
+            id="category"
+            name="category"
+            required
+            aria-label="Kategorie der Transaktion wählen"
+          >
+            <StyledFilterSelectOption value="">
+              -Bitte Kategorie ausw&auml;hlen-
+            </StyledFilterSelectOption>
+            {categories.map((cat) => (
+              <StyledFilterSelectOption key={cat} value={cat}>
+                {cat}
+              </StyledFilterSelectOption>
+            ))}
+          </StyledFilterSelect>
+        )}
+        {filterType === "date" && (
+          <StyledFilterInput
+            type="date"
+            name="date"
+            defaultValue={dayjs().format("YYYY-MM-DD")}
+            aria-label="Zeitpunkt der Transaktion wählen"
+          />
+        )}
+        <StyledFilterSubmit aria-label="Transaktionen filtern">
+          Filtern
+        </StyledFilterSubmit>
+      </StyledFilterForm>
+    </>
+  );
+}
+
+const StyledFilterForm = styled.form`
+  width: 300px;
+  display: flex;
+  justify-content: center;
+  flex-direction: column;
+  gap: var(--base);
+`;
+const StyledFilterInput = styled.input`
+  padding: var(--3xs);
+  line-height: 1.15;
+  font-size: 100%;
+`;
+
+const StyledFilterSubmit = styled.button`
+  padding: var(--3xs);
+  line-height: 1.15;
+  font-size: 100%;
+`;
+const StyledFilterSelect = styled.select`
+  padding: var(--3xs);
+  line-height: 1.15;
+  font-size: 100%;
+`;
+const StyledFilterSelectOption = styled.option``;
diff --git a/components/TransactionsHeader/index.js b/components/TransactionsHeader/index.js
new file mode 100644
index 0000000..66474a5
--- /dev/null
+++ b/components/TransactionsHeader/index.js
@@ -0,0 +1,46 @@
+import styled from "styled-components";
+import { CirclePlus } from "lucide-react";
+import { useModalContext } from "@/utils/ModalContext/ModalContext";
+
+export default function TransactionsHeader({ hasAddButton = false }) {
+  const { handleModalCall } = useModalContext();
+  return (
+    <>
+      <StyledHeaderWrapper>
+        <StyledH2>Transaktionen</StyledH2>
+        {hasAddButton && (
+          <StyledAddButton
+            onClick={handleModalCall}
+            aria-label="add a transaction"
+          >
+            <StyledCirclePlus />
+          </StyledAddButton>
+        )}
+      </StyledHeaderWrapper>
+    </>
+  );
+}
+
+const StyledH2 = styled.h2`
+  font-size: var(--2xl);
+  margin-bottom: 1rem;
+`;
+
+const StyledAddButton = styled.button`
+  background-color: transparent;
+  border: none;
+  color: var(--green-500);
+  width: 2.25rem;
+  height: 2.25rem;
+  cursor: pointer;
+`;
+
+const StyledCirclePlus = styled(CirclePlus)`
+  width: 100%;
+  height: 100%;
+`;
+
+const StyledHeaderWrapper = styled.div`
+  display: flex;
+  justify-content: space-between;
+`;
diff --git a/components/TransactionsList/index.js b/components/TransactionsList/index.js
index c0b7f5b..79f7313 100644
--- a/components/TransactionsList/index.js
+++ b/components/TransactionsList/index.js
@@ -1,23 +1,12 @@
 import dayjs from "dayjs";
 import styled from "styled-components";
 import TransactionCard from "../TransactionCard";
-import { CirclePlus } from "lucide-react";
-import { useModalContext } from "@/utils/ModalContext/ModalContext";
+
 import Link from "next/link";
 
 export default function TransactionsList({ transactions }) {
-  const { handleModalCall } = useModalContext();
   return (
     <>
-      <StyledHeaderWrapper>
-        <StyledH2>Transaktionen</StyledH2>
-        <StyledAddButton
-          onClick={handleModalCall}
-          aria-label="add a transaction"
-        >
-          <StyledCirclePlus />
-        </StyledAddButton>
-      </StyledHeaderWrapper>
       <StyledUl>
         {transactions.map(([isoDate, dayTransactions]) => {
           const formattedDate = dayjs(isoDate).format("DD.MM.YYYY");
@@ -64,25 +53,6 @@ const StyledLi = styled.li`
   gap: var(--3xs);
 `;
 
-const StyledAddButton = styled.button`
-  background-color: transparent;
-  border: none;
-  color: var(--green-500);
-  width: 2.25rem;
-  height: 2.25rem;
-  cursor: pointer;
-`;
-
-const StyledCirclePlus = styled(CirclePlus)`
-  width: 100%;
-  height: 100%;
-`;
-
-const StyledHeaderWrapper = styled.div`
-  display: flex;
-  justify-content: space-between;
-`;
-
 const TransactionCardLink = styled(Link)`
   all: unset;
   cursor: pointer;
diff --git a/db/dummy.json b/db/dummy.json
index 352866a..9a98914 100644
--- a/db/dummy.json
+++ b/db/dummy.json
@@ -2,7 +2,7 @@
   {
     "id": "1",
     "amount": -50.75,
-    "category": "Groceries",
+    "category": "Lebensmittel",
     "type": "expense",
     "date": "2025-04-01T00:00:00",
     "partner": "Lidl Dienstleistung GmbH & Co. KG"
@@ -10,7 +10,7 @@
   {
     "id": "2",
     "amount": -1200.0,
-    "category": "Rent",
+    "category": "Miete",
     "type": "expense",
     "date": "2024-07-02T00:00:00",
     "partner": "Michael Wendler"
@@ -18,7 +18,7 @@
   {
     "id": "3",
     "amount": 1500.0,
-    "category": "Salary",
+    "category": "Gehalt",
     "type": "income",
     "date": "2025-04-01T00:00:00",
     "partner": "Deutschland GmbH"
@@ -26,7 +26,7 @@
   {
     "id": "4",
     "amount": -200.5,
-    "category": "Insurance",
+    "category": "Versicherung",
     "type": "expense",
     "date": "2025-04-01T00:00:00",
     "partner": "AOK Deutschland AG"
@@ -34,7 +34,7 @@
   {
     "id": "5",
     "amount": -75.25,
-    "category": "Entertainment",
+    "category": "Unterhaltung",
     "type": "expense",
     "date": "2024-07-05T00:00:00",
     "partner": "Cineplex International Ltd."
@@ -50,7 +50,7 @@
   {
     "id": "7",
     "amount": 500.0,
-    "category": "Investment",
+    "category": "Investitionen",
     "type": "income",
     "date": "2024-07-07T00:00:00",
     "partner": "Trade Republic Aktien"
@@ -58,7 +58,7 @@
   {
     "id": "8",
     "amount": -30.0,
-    "category": "Transportation",
+    "category": "Transport",
     "type": "expense",
     "date": "2024-07-08T00:00:00",
     "partner": "Deutsche Bahn"
@@ -66,7 +66,7 @@
   {
     "id": "9",
     "amount": -100.0,
-    "category": "Education",
+    "category": "Bildung",
     "type": "expense",
     "date": "2024-07-09T00:00:00",
     "partner": "Wikipedia Foundation"
@@ -74,7 +74,7 @@
   {
     "id": "10",
     "amount": 200.0,
-    "category": "Savings",
+    "category": "Ersparnisse",
     "type": "income",
     "date": "2024-07-10T00:00:00",
     "partner": "Rente"
diff --git a/pages/analytics/index.js b/pages/analytics/index.js
new file mode 100644
index 0000000..fde59c2
--- /dev/null
+++ b/pages/analytics/index.js
@@ -0,0 +1,143 @@
+import TransactionsList from "@/components/TransactionsList/";
+import TransactionFilters from "@/components/TransactionFilters";
+import TransactionsHeader from "@/components/TransactionsHeader";
+import Modal from "@/components/Modal";
+import { useTransactionsContext } from "@/utils/TransactionsContext/TransactionsContext";
+import { useModalContext } from "@/utils/ModalContext/ModalContext";
+import {
+  groupTransactions,
+  filterTransactions,
+} from "@/utils/FilterFunctionsLib/filterFunctions";
+
+import styled from "styled-components";
+import { CalendarDays, Tag } from "lucide-react";
+import { useState } from "react";
+
+export default function AnalyticsPage() {
+  const { isLoading, sortedEntries, data } = useTransactionsContext();
+  const { modalOpen, handleModalCall } = useModalContext();
+  const [displayedEntries, setDisplayedEntries] = useState([...sortedEntries]);
+  const [appliedFilterType, setAppliedFilterType] = useState();
+  const [activeFilterType, setActiveFilterType] = useState();
+
+  function getTransactionsFiltered({
+    allTransactions,
+    filterCriterium,
+    filterPattern,
+  }) {
+    const filteredTransactions = filterTransactions({
+      allTransactions: [...allTransactions],
+      filterCriterium: filterCriterium,
+      filterPattern: filterPattern,
+    });
+    setActiveFilterType(appliedFilterType);
+    setDisplayedEntries([...groupTransactions(filteredTransactions)]);
+  }
+
+  function handleClickFilter(filterType) {
+    if (appliedFilterType !== filterType) {
+      setAppliedFilterType(filterType);
+      handleModalCall();
+    } else {
+      setAppliedFilterType();
+      setActiveFilterType();
+      setDisplayedEntries([...groupTransactions(data)]);
+    }
+  }
+
+  let applyModalFilterTitle;
+
+  switch (appliedFilterType) {
+    case "date":
+      applyModalFilterTitle = "Nach Datum filtern";
+      break;
+    case "category":
+      applyModalFilterTitle = "Nach Kategorie filtern";
+      break;
+    default:
+      applyModalFilterTitle = "";
+      break;
+  }
+
+  if (isLoading) return null;
+
+  return (
+    <>
+      {modalOpen && (
+        <Modal title={applyModalFilterTitle}>
+          <TransactionFilters
+            getTransactionsFiltered={getTransactionsFiltered}
+            filterType={appliedFilterType}
+          />
+        </Modal>
+      )}
+      <StyledH1>Analyse</StyledH1>
+      <StyledH2>Filtern nach:</StyledH2>
+      <StyledFilterCriteriaWrapper>
+        <StyledFilterButton onClick={() => handleClickFilter("category")}>
+          <IconTextWrapper>
+            <StyledTag
+              $activated={activeFilterType === "category" ? true : false}
+            ></StyledTag>
+          </IconTextWrapper>
+        </StyledFilterButton>
+        <StyledFilterButton onClick={() => handleClickFilter("date")}>
+          <IconTextWrapper>
+            <StyledCalendarDays
+              $activated={activeFilterType === "date" ? true : false}
+            ></StyledCalendarDays>
+          </IconTextWrapper>
+        </StyledFilterButton>
+      </StyledFilterCriteriaWrapper>
+      <TransactionsHeader />
+      {displayedEntries.length > 0 ? (
+        <TransactionsList transactions={displayedEntries} />
+      ) : (
+        <StyledH2>Keine Daten für den gew&auml;hlten Filter!</StyledH2>
+      )}
+    </>
+  );
+}
+
+const StyledH1 = styled.h1`
+  font-size: var(--3xl);
+  margin-bottom: 1rem;
+`;
+
+const StyledH2 = styled.h2`
+  font-size: var(--2xl);
+  margin-bottom: 1rem;
+`;
+
+const StyledFilterButton = styled.button`
+  background-color: transparent;
+  border: none;
+  cursor: pointer;
+`;
+
+const IconTextWrapper = styled.div`
+  display: flex;
+  gap: 1rem;
+`;
+
+const StyledCalendarDays = styled(CalendarDays)`
+  color: ${(props) =>
+    props.$activated ? `var(--red-500)` : `var(--green-500)`};
+  width: 35px;
+  height: 35px;
+  fill: ${(props) => (props.$activated ? `var(--green-500)` : "none")};
+`;
+
+const StyledTag = styled(Tag)`
+  color: ${(props) =>
+    props.$activated ? `var(--red-500)` : `var(--green-500)`};
+  width: 35px;
+  height: 35px;
+  fill: ${(props) => (props.$activated ? `var(--green-500)` : "none")};
+`;
+const StyledFilterCriteriaWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  gap: var(--xl);
+  padding: 1rem;
+`;
diff --git a/pages/index.js b/pages/index.js
index 254c91f..2883f9d 100644
--- a/pages/index.js
+++ b/pages/index.js
@@ -3,6 +3,7 @@ import { useTransactionsContext } from "@/utils/TransactionsContext/Transactions
 import { useModalContext } from "@/utils/ModalContext/ModalContext";
 import TransactionsList from "@/components/TransactionsList/";
 import TransactionForm from "@/components/TransactionForm";
+import TransactionsHeader from "@/components/TransactionsHeader";
 import Modal from "@/components/Modal";
 
 export default function HomePage() {
@@ -19,6 +20,7 @@ export default function HomePage() {
         </Modal>
       )}
       <AccountBalance transactions={data} />
+      <TransactionsHeader hasAddButton />
       <TransactionsList transactions={sortedEntries} />
     </>
   );
diff --git a/utils/FilterFunctionsLib/filterFunctions.js b/utils/FilterFunctionsLib/filterFunctions.js
new file mode 100644
index 0000000..4357f05
--- /dev/null
+++ b/utils/FilterFunctionsLib/filterFunctions.js
@@ -0,0 +1,36 @@
+import dayjs from "dayjs";
+
+export function groupTransactions(allTransactions) {
+  const groupedByDate = Object.groupBy(allTransactions, (transaction) =>
+    dayjs(transaction.date).format("YYYY.MM.DD")
+  );
+  const sortedEntries = Object.entries(groupedByDate).sort(
+    ([dateA], [dateB]) => {
+      return dayjs(dateB).valueOf() - dayjs(dateA).valueOf();
+    }
+  );
+
+  return sortedEntries;
+}
+
+export function filterTransactions({
+  allTransactions,
+  filterCriterium,
+  filterOperator = "===",
+  filterPattern,
+}) {
+  const filteredEntries = allTransactions.filter((item) => {
+    let filterString;
+    switch (filterCriterium) {
+      case "date":
+        filterString = `item.date.startsWith("${filterPattern}")`;
+        break;
+      default:
+        filterString = `"${item[filterCriterium]}" ${filterOperator} "${filterPattern}"`;
+        break;
+    }
+    const result = eval(filterString);
+    return result;
+  });
+  return filteredEntries;
+}
diff --git a/utils/TransactionsContext/TransactionsContext.js b/utils/TransactionsContext/TransactionsContext.js
index 32e6a65..fbe9573 100644
--- a/utils/TransactionsContext/TransactionsContext.js
+++ b/utils/TransactionsContext/TransactionsContext.js
@@ -1,6 +1,6 @@
 import { useContext, createContext } from "react";
+import { groupTransactions } from "@/utils/FilterFunctionsLib/filterFunctions";
 import useSWR from "swr";
-import dayjs from "dayjs";
 
 const fetcher = (url) => fetch(url).then((r) => r.json());
 
@@ -16,13 +16,7 @@ export function TransactionsProvider({ children }) {
   if (!data) {
     return null;
   }
-
-  const dataFixed = Object.groupBy(data, (transaction) =>
-    dayjs(transaction.date).format("YYYY.MM.DD")
-  );
-  const sortedEntries = Object.entries(dataFixed).sort(([dateA], [dateB]) => {
-    return dayjs(dateB).valueOf() - dayjs(dateA).valueOf();
-  });
+  const sortedEntries = groupTransactions(data);
 
   return (
     <TransactionsContext.Provider


Reviews:
user: login: Roland-Hufnagel
body: The analytics page is really too complicated. It was not fun to review. I can promise a lot of troubles while the app grows. 'Keep it simple' is a good advice. The datastructure you use is not easy to handle, too. Filtering a three dimensional Array will of cause not be easy. 
Filtering a list of transactions would be a very simple implementation. Maybe you can filter the list first and do the grouping only for the rendering. 

It is up to you but I strongly recommend to refactor the code to a more simple and better maintainable solution. 

Nevertheless I left some other comments.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T16:11:34Z
user: login: niqeff
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T18:35:56Z
user: login: niqeff
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T18:36:12Z
user: login: niqeff
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T18:36:24Z
user: login: niqeff
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T18:36:52Z
user: login: niqeff
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T18:37:31Z
user: login: Roland-Hufnagel
body: I left one more suggestion - but you don't have to implement it.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T13:35:20Z

Review comments:
diff_hunk: @@ -0,0 +1,139 @@
+import TransactionsList from "@/components/TransactionsList/";
+import TransactionFilters from "@/components/TransactionFilters";
+import Modal from "@/components/Modal";
+import { useTransactionsContext } from "@/utils/TransactionsContext/TransactionsContext";
+import { useModalContext } from "@/utils/ModalContext/ModalContext";
+import {
+  groupTransactions,
+  filterTransactions,
+} from "@/utils/FilterFunctionsLib/filterFunctions";
+
+import styled from "styled-components";
+import { CalendarDays, Tag } from "lucide-react";
+import { useState } from "react";
+
+export default function AnalyticsPage() {
+  const { isLoading, sortedEntries, data } = useTransactionsContext();
+  const { modalOpen, handleModalCall } = useModalContext();
+  const [displayedEntries, setDisplayedEntries] = useState([...sortedEntries]);
+  const [useFilterType, setUseFilterType] = useState();
+
+  function getTransactionsFiltered({
+    allTransactions,
+    filterCriterium,
+    filterPattern,
+  }) {
+    const filteredTransactions = filterTransactions({
+      allTransactions: [...allTransactions],
+      filterCriterium: filterCriterium,
+      filterPattern: filterPattern,
+    });
+
+    setDisplayedEntries([...groupTransactions(filteredTransactions)]);
+  }
+
+  function handleClickFilter(filterType) {
+    if (useFilterType !== filterType) {
+      setUseFilterType(filterType);
+      handleModalCall();
+    } else {
+      setUseFilterType();
+      setDisplayedEntries([...groupTransactions(data)]);
+    }
+  }
+
+  let useModalFilterTitle;
+
+  switch (useFilterType) {
+    case "date":
+      useModalFilterTitle = "Nach Datum filtern";
+      break;
+    case "category":
+      useModalFilterTitle = "Nach Kategorie filtern";
+      break;
+    default:
+      useModalFilterTitle = "";
+      break;
+  }
+
+  if (isLoading) return null;
+
+  return (
+    <>
+      {modalOpen && (
+        <Modal title={useModalFilterTitle}>
+          <TransactionFilters
+            getTransactionsFiltered={getTransactionsFiltered}
+            filterType={useFilterType}
+          />
+        </Modal>
+      )}
+      <StyledH1>Analyse</StyledH1>
+      <StyledH2>Filtern nach:</StyledH2>
+      <StyledFilterCriteriaWrapper>
+        <StyledFilterButton onClick={() => handleClickFilter("category")}>
+          <IconTextWrapper>
+            <StyledTag
+              activated={useFilterType === "category" ? true : false}
+            ></StyledTag>
+          </IconTextWrapper>
+        </StyledFilterButton>
+        <StyledFilterButton onClick={() => handleClickFilter("date")}>
+          <IconTextWrapper>
+            <StyledCalendarDays
+              activated={useFilterType === "date" ? true : false}
+            ></StyledCalendarDays>
+          </IconTextWrapper>
+        </StyledFilterButton>
+      </StyledFilterCriteriaWrapper>
+
+      {displayedEntries.length > 0 ? (
+        <TransactionsList transactions={displayedEntries} />
+      ) : (
+        <StyledH2>Keine Daten für den gew&auml;hlten Filter!</StyledH2>
+      )}
+    </>
+  );
+}
+
+const StyledH1 = styled.h1`
+  font-size: var(--3xl);
+  margin-bottom: 1rem;
+`;
+
+const StyledH2 = styled.h2`
+  font-size: var(--2xl);
+  margin-bottom: 1rem;
+`;
+
+const StyledFilterButton = styled.button`
+  background-color: transparent;
+  border: none;
path: pages/analytics/index.js
user: login: Roland-Hufnagel
body: I recommend 'cursor: pointer' to reflect it's purpose
created_at: 2025-04-07T15:08:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,139 @@
+import TransactionsList from "@/components/TransactionsList/";
+import TransactionFilters from "@/components/TransactionFilters";
+import Modal from "@/components/Modal";
+import { useTransactionsContext } from "@/utils/TransactionsContext/TransactionsContext";
+import { useModalContext } from "@/utils/ModalContext/ModalContext";
+import {
+  groupTransactions,
+  filterTransactions,
+} from "@/utils/FilterFunctionsLib/filterFunctions";
+
+import styled from "styled-components";
+import { CalendarDays, Tag } from "lucide-react";
+import { useState } from "react";
+
+export default function AnalyticsPage() {
+  const { isLoading, sortedEntries, data } = useTransactionsContext();
+  const { modalOpen, handleModalCall } = useModalContext();
+  const [displayedEntries, setDisplayedEntries] = useState([...sortedEntries]);
+  const [useFilterType, setUseFilterType] = useState();
+
+  function getTransactionsFiltered({
+    allTransactions,
+    filterCriterium,
+    filterPattern,
+  }) {
+    const filteredTransactions = filterTransactions({
+      allTransactions: [...allTransactions],
+      filterCriterium: filterCriterium,
+      filterPattern: filterPattern,
+    });
+
+    setDisplayedEntries([...groupTransactions(filteredTransactions)]);
+  }
+
+  function handleClickFilter(filterType) {
+    if (useFilterType !== filterType) {
+      setUseFilterType(filterType);
+      handleModalCall();
+    } else {
+      setUseFilterType();
+      setDisplayedEntries([...groupTransactions(data)]);
+    }
+  }
+
+  let useModalFilterTitle;
+
+  switch (useFilterType) {
+    case "date":
+      useModalFilterTitle = "Nach Datum filtern";
+      break;
+    case "category":
+      useModalFilterTitle = "Nach Kategorie filtern";
+      break;
+    default:
+      useModalFilterTitle = "";
+      break;
+  }
+
+  if (isLoading) return null;
+
+  return (
+    <>
+      {modalOpen && (
+        <Modal title={useModalFilterTitle}>
+          <TransactionFilters
+            getTransactionsFiltered={getTransactionsFiltered}
+            filterType={useFilterType}
+          />
+        </Modal>
+      )}
+      <StyledH1>Analyse</StyledH1>
+      <StyledH2>Filtern nach:</StyledH2>
+      <StyledFilterCriteriaWrapper>
+        <StyledFilterButton onClick={() => handleClickFilter("category")}>
+          <IconTextWrapper>
+            <StyledTag
+              activated={useFilterType === "category" ? true : false}
path: pages/analytics/index.js
user: login: Roland-Hufnagel
body: Please use the $-prefix to not push the prop to the DOM.
created_at: 2025-04-07T15:11:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,139 @@
+import TransactionsList from "@/components/TransactionsList/";
+import TransactionFilters from "@/components/TransactionFilters";
+import Modal from "@/components/Modal";
+import { useTransactionsContext } from "@/utils/TransactionsContext/TransactionsContext";
+import { useModalContext } from "@/utils/ModalContext/ModalContext";
+import {
+  groupTransactions,
+  filterTransactions,
+} from "@/utils/FilterFunctionsLib/filterFunctions";
+
+import styled from "styled-components";
+import { CalendarDays, Tag } from "lucide-react";
+import { useState } from "react";
+
+export default function AnalyticsPage() {
+  const { isLoading, sortedEntries, data } = useTransactionsContext();
+  const { modalOpen, handleModalCall } = useModalContext();
+  const [displayedEntries, setDisplayedEntries] = useState([...sortedEntries]);
+  const [useFilterType, setUseFilterType] = useState();
path: pages/analytics/index.js
user: login: Roland-Hufnagel
body: Don't use 'use' as a prefix. This looks like it is a custom hook. 'filterType' is enough.
created_at: 2025-04-07T15:14:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,139 @@
+import TransactionsList from "@/components/TransactionsList/";
+import TransactionFilters from "@/components/TransactionFilters";
+import Modal from "@/components/Modal";
+import { useTransactionsContext } from "@/utils/TransactionsContext/TransactionsContext";
+import { useModalContext } from "@/utils/ModalContext/ModalContext";
+import {
+  groupTransactions,
+  filterTransactions,
+} from "@/utils/FilterFunctionsLib/filterFunctions";
+
+import styled from "styled-components";
+import { CalendarDays, Tag } from "lucide-react";
+import { useState } from "react";
+
+export default function AnalyticsPage() {
+  const { isLoading, sortedEntries, data } = useTransactionsContext();
+  const { modalOpen, handleModalCall } = useModalContext();
+  const [displayedEntries, setDisplayedEntries] = useState([...sortedEntries]);
+  const [useFilterType, setUseFilterType] = useState();
+
+  function getTransactionsFiltered({
+    allTransactions,
+    filterCriterium,
+    filterPattern,
+  }) {
+    const filteredTransactions = filterTransactions({
+      allTransactions: [...allTransactions],
+      filterCriterium: filterCriterium,
+      filterPattern: filterPattern,
+    });
+
+    setDisplayedEntries([...groupTransactions(filteredTransactions)]);
+  }
+
+  function handleClickFilter(filterType) {
path: pages/analytics/index.js
user: login: Roland-Hufnagel
body: a QA point in advance: This works not well. Cancelling the modal leads to unexpected style changes.
created_at: 2025-04-07T15:18:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,139 @@
+import TransactionsList from "@/components/TransactionsList/";
+import TransactionFilters from "@/components/TransactionFilters";
+import Modal from "@/components/Modal";
+import { useTransactionsContext } from "@/utils/TransactionsContext/TransactionsContext";
+import { useModalContext } from "@/utils/ModalContext/ModalContext";
+import {
+  groupTransactions,
+  filterTransactions,
+} from "@/utils/FilterFunctionsLib/filterFunctions";
+
+import styled from "styled-components";
+import { CalendarDays, Tag } from "lucide-react";
+import { useState } from "react";
+
+export default function AnalyticsPage() {
+  const { isLoading, sortedEntries, data } = useTransactionsContext();
+  const { modalOpen, handleModalCall } = useModalContext();
+  const [displayedEntries, setDisplayedEntries] = useState([...sortedEntries]);
+  const [useFilterType, setUseFilterType] = useState();
+
+  function getTransactionsFiltered({
+    allTransactions,
+    filterCriterium,
+    filterPattern,
+  }) {
+    const filteredTransactions = filterTransactions({
+      allTransactions: [...allTransactions],
+      filterCriterium: filterCriterium,
+      filterPattern: filterPattern,
+    });
+
+    setDisplayedEntries([...groupTransactions(filteredTransactions)]);
+  }
+
+  function handleClickFilter(filterType) {
+    if (useFilterType !== filterType) {
+      setUseFilterType(filterType);
+      handleModalCall();
+    } else {
+      setUseFilterType();
+      setDisplayedEntries([...groupTransactions(data)]);
+    }
+  }
+
+  let useModalFilterTitle;
path: pages/analytics/index.js
user: login: Roland-Hufnagel
body: don't use 'use' as a prefix (to distinct from custom hooks).
created_at: 2025-04-07T15:33:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,18 +5,23 @@ import { CirclePlus } from "lucide-react";
 import { useModalContext } from "@/utils/ModalContext/ModalContext";
 import Link from "next/link";
 
-export default function TransactionsList({ transactions }) {
+export default function TransactionsList({
+  transactions,
+  hasAddButton = false,
path: components/TransactionsList/index.js
user: login: Roland-Hufnagel
body: A better solution would be if the AddButton in not a part of the list. The component 'TransactionList' should ideally only handle to render a list of transactions. Whereever you want to use it.
created_at: 2025-04-07T15:59:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,139 @@
+import TransactionsList from "@/components/TransactionsList/";
+import TransactionFilters from "@/components/TransactionFilters";
+import Modal from "@/components/Modal";
+import { useTransactionsContext } from "@/utils/TransactionsContext/TransactionsContext";
+import { useModalContext } from "@/utils/ModalContext/ModalContext";
+import {
+  groupTransactions,
+  filterTransactions,
+} from "@/utils/FilterFunctionsLib/filterFunctions";
+
+import styled from "styled-components";
+import { CalendarDays, Tag } from "lucide-react";
+import { useState } from "react";
+
+export default function AnalyticsPage() {
+  const { isLoading, sortedEntries, data } = useTransactionsContext();
+  const { modalOpen, handleModalCall } = useModalContext();
+  const [displayedEntries, setDisplayedEntries] = useState([...sortedEntries]);
+  const [useFilterType, setUseFilterType] = useState();
+
+  function getTransactionsFiltered({
+    allTransactions,
+    filterCriterium,
+    filterPattern,
+  }) {
+    const filteredTransactions = filterTransactions({
+      allTransactions: [...allTransactions],
+      filterCriterium: filterCriterium,
+      filterPattern: filterPattern,
+    });
+
+    setDisplayedEntries([...groupTransactions(filteredTransactions)]);
+  }
+
+  function handleClickFilter(filterType) {
+    if (useFilterType !== filterType) {
+      setUseFilterType(filterType);
+      handleModalCall();
+    } else {
+      setUseFilterType();
+      setDisplayedEntries([...groupTransactions(data)]);
+    }
+  }
+
+  let useModalFilterTitle;
+
+  switch (useFilterType) {
+    case "date":
+      useModalFilterTitle = "Nach Datum filtern";
+      break;
+    case "category":
+      useModalFilterTitle = "Nach Kategorie filtern";
+      break;
+    default:
+      useModalFilterTitle = "";
+      break;
+  }
+
+  if (isLoading) return null;
+
+  return (
+    <>
+      {modalOpen && (
+        <Modal title={useModalFilterTitle}>
+          <TransactionFilters
+            getTransactionsFiltered={getTransactionsFiltered}
+            filterType={useFilterType}
+          />
+        </Modal>
+      )}
+      <StyledH1>Analyse</StyledH1>
+      <StyledH2>Filtern nach:</StyledH2>
+      <StyledFilterCriteriaWrapper>
+        <StyledFilterButton onClick={() => handleClickFilter("category")}>
+          <IconTextWrapper>
+            <StyledTag
+              activated={useFilterType === "category" ? true : false}
+            ></StyledTag>
+          </IconTextWrapper>
+        </StyledFilterButton>
+        <StyledFilterButton onClick={() => handleClickFilter("date")}>
+          <IconTextWrapper>
+            <StyledCalendarDays
+              activated={useFilterType === "date" ? true : false}
+            ></StyledCalendarDays>
+          </IconTextWrapper>
+        </StyledFilterButton>
+      </StyledFilterCriteriaWrapper>
+
+      {displayedEntries.length > 0 ? (
+        <TransactionsList transactions={displayedEntries} />
+      ) : (
+        <StyledH2>Keine Daten für den gew&auml;hlten Filter!</StyledH2>
+      )}
+    </>
+  );
+}
+
+const StyledH1 = styled.h1`
+  font-size: var(--3xl);
+  margin-bottom: 1rem;
+`;
+
+const StyledH2 = styled.h2`
+  font-size: var(--2xl);
+  margin-bottom: 1rem;
+`;
+
+const StyledFilterButton = styled.button`
+  background-color: transparent;
+  border: none;
path: pages/analytics/index.js
user: login: niqeff
body: 👍
created_at: 2025-04-07T18:35:56Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,139 @@
+import TransactionsList from "@/components/TransactionsList/";
+import TransactionFilters from "@/components/TransactionFilters";
+import Modal from "@/components/Modal";
+import { useTransactionsContext } from "@/utils/TransactionsContext/TransactionsContext";
+import { useModalContext } from "@/utils/ModalContext/ModalContext";
+import {
+  groupTransactions,
+  filterTransactions,
+} from "@/utils/FilterFunctionsLib/filterFunctions";
+
+import styled from "styled-components";
+import { CalendarDays, Tag } from "lucide-react";
+import { useState } from "react";
+
+export default function AnalyticsPage() {
+  const { isLoading, sortedEntries, data } = useTransactionsContext();
+  const { modalOpen, handleModalCall } = useModalContext();
+  const [displayedEntries, setDisplayedEntries] = useState([...sortedEntries]);
+  const [useFilterType, setUseFilterType] = useState();
+
+  function getTransactionsFiltered({
+    allTransactions,
+    filterCriterium,
+    filterPattern,
+  }) {
+    const filteredTransactions = filterTransactions({
+      allTransactions: [...allTransactions],
+      filterCriterium: filterCriterium,
+      filterPattern: filterPattern,
+    });
+
+    setDisplayedEntries([...groupTransactions(filteredTransactions)]);
+  }
+
+  function handleClickFilter(filterType) {
+    if (useFilterType !== filterType) {
+      setUseFilterType(filterType);
+      handleModalCall();
+    } else {
+      setUseFilterType();
+      setDisplayedEntries([...groupTransactions(data)]);
+    }
+  }
+
+  let useModalFilterTitle;
+
+  switch (useFilterType) {
+    case "date":
+      useModalFilterTitle = "Nach Datum filtern";
+      break;
+    case "category":
+      useModalFilterTitle = "Nach Kategorie filtern";
+      break;
+    default:
+      useModalFilterTitle = "";
+      break;
+  }
+
+  if (isLoading) return null;
+
+  return (
+    <>
+      {modalOpen && (
+        <Modal title={useModalFilterTitle}>
+          <TransactionFilters
+            getTransactionsFiltered={getTransactionsFiltered}
+            filterType={useFilterType}
+          />
+        </Modal>
+      )}
+      <StyledH1>Analyse</StyledH1>
+      <StyledH2>Filtern nach:</StyledH2>
+      <StyledFilterCriteriaWrapper>
+        <StyledFilterButton onClick={() => handleClickFilter("category")}>
+          <IconTextWrapper>
+            <StyledTag
+              activated={useFilterType === "category" ? true : false}
path: pages/analytics/index.js
user: login: niqeff
body: 👍
created_at: 2025-04-07T18:36:12Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,139 @@
+import TransactionsList from "@/components/TransactionsList/";
+import TransactionFilters from "@/components/TransactionFilters";
+import Modal from "@/components/Modal";
+import { useTransactionsContext } from "@/utils/TransactionsContext/TransactionsContext";
+import { useModalContext } from "@/utils/ModalContext/ModalContext";
+import {
+  groupTransactions,
+  filterTransactions,
+} from "@/utils/FilterFunctionsLib/filterFunctions";
+
+import styled from "styled-components";
+import { CalendarDays, Tag } from "lucide-react";
+import { useState } from "react";
+
+export default function AnalyticsPage() {
+  const { isLoading, sortedEntries, data } = useTransactionsContext();
+  const { modalOpen, handleModalCall } = useModalContext();
+  const [displayedEntries, setDisplayedEntries] = useState([...sortedEntries]);
+  const [useFilterType, setUseFilterType] = useState();
path: pages/analytics/index.js
user: login: niqeff
body: 👍
created_at: 2025-04-07T18:36:24Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,139 @@
+import TransactionsList from "@/components/TransactionsList/";
+import TransactionFilters from "@/components/TransactionFilters";
+import Modal from "@/components/Modal";
+import { useTransactionsContext } from "@/utils/TransactionsContext/TransactionsContext";
+import { useModalContext } from "@/utils/ModalContext/ModalContext";
+import {
+  groupTransactions,
+  filterTransactions,
+} from "@/utils/FilterFunctionsLib/filterFunctions";
+
+import styled from "styled-components";
+import { CalendarDays, Tag } from "lucide-react";
+import { useState } from "react";
+
+export default function AnalyticsPage() {
+  const { isLoading, sortedEntries, data } = useTransactionsContext();
+  const { modalOpen, handleModalCall } = useModalContext();
+  const [displayedEntries, setDisplayedEntries] = useState([...sortedEntries]);
+  const [useFilterType, setUseFilterType] = useState();
+
+  function getTransactionsFiltered({
+    allTransactions,
+    filterCriterium,
+    filterPattern,
+  }) {
+    const filteredTransactions = filterTransactions({
+      allTransactions: [...allTransactions],
+      filterCriterium: filterCriterium,
+      filterPattern: filterPattern,
+    });
+
+    setDisplayedEntries([...groupTransactions(filteredTransactions)]);
+  }
+
+  function handleClickFilter(filterType) {
path: pages/analytics/index.js
user: login: niqeff
body: 👍
created_at: 2025-04-07T18:36:52Z
author_association: COLLABORATOR
diff_hunk: @@ -5,18 +5,23 @@ import { CirclePlus } from "lucide-react";
 import { useModalContext } from "@/utils/ModalContext/ModalContext";
 import Link from "next/link";
 
-export default function TransactionsList({ transactions }) {
+export default function TransactionsList({
+  transactions,
+  hasAddButton = false,
path: components/TransactionsList/index.js
user: login: niqeff
body: 👍
created_at: 2025-04-07T18:37:31Z
author_association: COLLABORATOR
diff_hunk: @@ -19,6 +20,7 @@ export default function HomePage() {
         </Modal>
       )}
       <AccountBalance transactions={data} />
+      <TransactionsHeader hasAddButton />
path: pages/index.js
user: login: Roland-Hufnagel
body: Now you apply the same concept as before - only in a different component. 
Since you want to see it on the homepage you should render it there. 
(just a suggestion to keep the code clean and to seperate concerns)
created_at: 2025-04-08T13:33:54Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-07T10:17:22Z
body: [vc]: #X6gzTC0RXxd2+Ex9LIpmMgzhXIzG0dp4sSGK7De2woQ=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJtb24tZXkiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoibW9uLWV5LWdpdC1mZWF0dXJlLWZpbHRlci1jNzI3YWUtanVsaWFuLWR1cnJzLXByb2plY3RzLTA5OTNjN2U3LnZlcmNlbC5hcHAifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2p1bGlhbi1kdXJycy1wcm9qZWN0cy0wOTkzYzdlNy9tb24tZXkvQWZwakdXR2R4ZGROeHBkMTI3WGJDU0UzU0xqdiIsInByZXZpZXdVcmwiOiJtb24tZXktZ2l0LWZlYXR1cmUtZmlsdGVyLWM3MjdhZS1qdWxpYW4tZHVycnMtcHJvamVjdHMtMDk5M2M3ZTcudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsInJvb3REaXJlY3RvcnkiOm51bGx9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **mon-ey** | ✅ Ready ([Inspect](https://vercel.com/julian-durrs-projects-0993c7e7/mon-ey/AfpjGWGdxddNxpd127XbCSE3SLjv)) | [Visit Preview](https://mon-ey-git-feature-filter-c727ae-julian-durrs-projects-0993c7e7.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/mon-ey-git-feature-filter-c727ae-julian-durrs-projects-0993c7e7.vercel.app?via=pr-comment-feedback-link) | Apr 7, 2025 6:48pm |


author_association: NONE
user: login: Roland-Hufnagel
created_at: 2025-04-07T14:06:46Z
body: Why is the US already closed?
author_association: CONTRIBUTOR
user: login: niqeff
created_at: 2025-04-07T14:12:49Z
body: > Why is the US already closed?

Sorry! I used github functionality, that moves closed issues from "in progress" to "code review" column in the project board. Reopened again now...
author_association: COLLABORATOR
user: login: niqeff
created_at: 2025-04-07T18:45:24Z
body: > The analytics page is really too complicated. It was not fun to review. I can promise a lot of troubles while the app grows. 'Keep it simple' is a good advice. The datastructure you use is not easy to handle, too. Filtering a three dimensional Array will of cause not be easy. Filtering a list of transactions would be a very simple implementation. Maybe you can filter the list first and do the grouping only for the rendering.
> 
In fact this in mainly what is done in the filter functions. Filtering is executed in a generalistic way in order to improve reusability - e.g. US "Expense/Income View" will be able use those functions as well without changes. And the two step approach you mentioned is implemented though. For the time being, we will continue with this approach, but with your admonishing words and a justified “I told you so...” in mind.
author_association: COLLABORATOR