diff --git a/package-lock.json b/package-lock.json index aa0a8a9..c65656a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@types/papaparse": "^5.3.14", + "@types/strftime": "^0.9.8", "bootstrap": "^5.3.3", "firebase": "^10.11.0", "lodash": "^4.17.21", @@ -19,7 +20,8 @@ "react-router-dom": "^6.23.0", "react-tiles-dnd": "^0.1.2", "recharts": "^2.12.5", - "sass": "^1.72.0" + "sass": "^1.72.0", + "strftime": "^0.10.2" }, "devDependencies": { "@faker-js/faker": "^8.4.1", @@ -2584,6 +2586,11 @@ "@types/send": "*" } }, + "node_modules/@types/strftime": { + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@types/strftime/-/strftime-0.9.8.tgz", + "integrity": "sha512-QIvDlGAKyF3YJbT3QZnfC+RIvV5noyDbi+ZJ5rkaSRqxCGrYJefgXm3leZAjtoQOutZe1hCXbAg+p89/Vj4HlQ==" + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -6705,6 +6712,14 @@ "dev": true, "optional": true }, + "node_modules/strftime": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/strftime/-/strftime-0.10.2.tgz", + "integrity": "sha512-Y6IZaTVM80chcMe7j65Gl/0nmlNdtt+KWPle5YeCAjmsBfw+id2qdaJ5MDrxUq+OmHKab+jHe7mUjU/aNMSZZg==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", diff --git a/package.json b/package.json index caaae65..4dce34f 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@types/papaparse": "^5.3.14", + "@types/strftime": "^0.9.8", "bootstrap": "^5.3.3", "firebase": "^10.11.0", "lodash": "^4.17.21", @@ -23,7 +24,8 @@ "react-router-dom": "^6.23.0", "react-tiles-dnd": "^0.1.2", "recharts": "^2.12.5", - "sass": "^1.72.0" + "sass": "^1.72.0", + "strftime": "^0.10.2" }, "devDependencies": { "@faker-js/faker": "^8.4.1", diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx index 03e5549..f8b4c78 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/pages/dashboard/DashboardPage.tsx @@ -3,7 +3,7 @@ import "react-tiles-dnd/esm/index.css"; import { TilesContainer, RenderTileFunction } from "react-tiles-dnd"; import useWindowDimensions from "../../hooks/WindowDimensionsHook.tsx"; import {Header} from "../../components/Header.tsx"; -import React, {ReactNode, useEffect, useState} from "react"; +import {ReactNode, useEffect, useState} from "react"; import { getTransactionsFilterOrderBy, Transaction } from "../../utils/transaction.ts" import {auth} from "../../utils/firebase.ts"; import {orderBy} from "firebase/firestore"; diff --git a/src/pages/dashboard/GraphUtils.ts b/src/pages/dashboard/GraphUtils.ts index 301cc3c..266777c 100644 --- a/src/pages/dashboard/GraphUtils.ts +++ b/src/pages/dashboard/GraphUtils.ts @@ -2,13 +2,14 @@ import {Transaction} from "../../utils/transaction.ts"; type transactionPoint = { date: string; amount: number } -const cumulateTransactions = (points: transactionPoint[]): transactionPoint[] => { - let total = 0; - return points.map(value => { - total += value.amount; - return {date: value.date, amount: total}; - }) -} +// const cumulateTransactions = (points: transactionPoint[]): transactionPoint[] => { +// let total = 0; +// return points.map(value => { +// total += value.amount; +// return {date: value.date, amount: total}; +// }) +// } + const getDateString = (timestamp: number): string => { const date = new Date(timestamp) const day = date.getDate().toString().padStart(2, '0'); // Ensures two digits diff --git a/src/pages/transactions/TransactionPage.tsx b/src/pages/transactions/TransactionPage.tsx new file mode 100644 index 0000000..f7ecfde --- /dev/null +++ b/src/pages/transactions/TransactionPage.tsx @@ -0,0 +1,177 @@ +import {useState} from "react"; +import {auth} from "../../utils/firebase.ts"; +import {getTransactionsFilterOrderBy, Transaction} from "../../utils/transaction.ts"; +import {FullscreenCenter} from "../../components/FullscreenCenter.tsx"; +import {User} from "firebase/auth"; +import {Header} from "../../components/Header.tsx"; +import {signInWithGoogle} from "../../utils/authentication.ts"; +import {limit, orderBy, startAfter} from "firebase/firestore"; +import strftime from "strftime"; + +// CSS +import "./transactionsTable.scss"; + +export function TransactionPage() { + const [transactions, setTransactions] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [authResolved, setAuthResolved] = useState(false); + const [update, setUpdate] = useState(0); + const [pageStarts, setPageStarts] = useState([Infinity]); + const itemsPerPage = 14; + + + if (!authResolved) { + auth.authStateReady().then(() => setAuthResolved(true)); + return <> + +
+

Waiting for Auth

+
+
+ ; + } + + if (auth.currentUser === null) { + auth.onAuthStateChanged((new_user: User | null) => { + if (new_user !== null) { + setUpdate(update + 1); + } + }); + return <> +
+ +
+

Not Logged In

+ +
+
+ ; + } + + if (!transactions) { + getTransactionsFilterOrderBy(auth.currentUser, orderBy("dateTime", "desc"), limit(itemsPerPage), startAfter(pageStarts[pageStarts.length - 1])) + .then((pageTransactions) => { + setTransactions(pageTransactions); + }); + + return <> +
+ + + + + + + + + + + + + + + + + + + + + +
NameCategoryEmojiDateAmountNotes
Fetching...Fetching...Fetching...Fetching...Fetching...Fetching...
+ ; + } + + + // adjusted dylan.s code to use getTransactionPage instead of getTransactions + // useEffect(() => { + // getTransactionsPage(auth.currentUser, itemsPerPage, currentPage) + // .then((pageTransactions) => { + // console.log("Fetched transactions:"); + // console.log(pageTransactions); + // setTransactions(pageTransactions); + // }); + // }, [currentPage]); + + return <> +
+
+ + + + + + + + + + + + + {transactions.map((transaction) => ( + + // maps every transaction as a row in the table + ))} + +
NameCategoryEmojiDateAmountDescription
+ { transactions.length == 0 && +

[No More Data]

+ } +
+ {/* conditional previous page button, only displayed if page number > 1 */} + {currentPage > 0 && ( + + )} + {/* spacers to push buttons to the edges */} +
+ Page {currentPage + 1} +
+ {transactions.length === itemsPerPage && + + } +
+ +
+ ; +} + +function TransactionItem({data}: { data: Transaction }) { + return ( + + {data.name} + {data.category} + {data.emoji} + {strftime("%d/%m/%y - %H:%M", new Date(data.dateTime))} + {data.amount > 0 ? + +
+ £ + {data.amount.toFixed(2)} +
+ : + +
+ £ + {data.amount.toFixed(2)} +
+ + } + {data.description} + + ); +} \ No newline at end of file diff --git a/src/pages/transactions/transactionsTable.css b/src/pages/transactions/transactionsTable.css new file mode 100644 index 0000000..2eb9216 --- /dev/null +++ b/src/pages/transactions/transactionsTable.css @@ -0,0 +1,69 @@ +/* page background */ +body { + background-color: #e6f2ff; +} + +/* table */ +table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + border-radius: 10px; /* rounded borders */ + background-color: #ffffff; + overflow: hidden; /* hide overflow from rounded borders */ +} + +th { + padding: 12px 15px; + background-color: #f2f2f2; + border-bottom: 1px solid transparent; /* transparent border for rows */ +} + +td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid transparent; /* transparent border for rows */ +} + +/* highlight on hover for table rows */ +tbody tr:hover { + background-color: #f5f5f5; +} + +/* Pagination */ +.pagination { + margin-top: 20px; + display: flex; + align-items: center; /* align items vertically */ +} + +.pagination button { + padding: 8px 12px; + border: 1px solid #007bff; + background-color: #007bff; + color: white; + cursor: pointer; + border-radius: 5px; +} + +.pagination .spacer { + flex-grow: 1; /* grow to fill available space */ +} + +.page-counter { + margin: 0 10px; /* add margin to separate from buttons */ +} + +/* hover effect for buttons */ +.pagination button:hover { + background-color: #0056b3; +} + +/* disabled state for buttons */ +.pagination button:disabled { + background-color: #cccccc; + border-color: #cccccc; + cursor: not-allowed; +} + +/*# sourceMappingURL=transactionsTable.css.map */ diff --git a/src/pages/transactions/transactionsTable.css.map b/src/pages/transactions/transactionsTable.css.map new file mode 100644 index 0000000..30fecee --- /dev/null +++ b/src/pages/transactions/transactionsTable.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["transactionsTable.scss"],"names":[],"mappings":"AAAA;AACA;EACE;;;AAGF;AACA;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAKF;EACE;EACA;EACA;;;AAGF;AACA;EACE;;;AAGF;AACA;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;AACA;EACE;;;AAGF;AACA;EACE;EACA;EACA","file":"transactionsTable.css"} \ No newline at end of file diff --git a/src/pages/transactions/transactionsTable.scss b/src/pages/transactions/transactionsTable.scss new file mode 100644 index 0000000..b8e0aeb --- /dev/null +++ b/src/pages/transactions/transactionsTable.scss @@ -0,0 +1,69 @@ +/* page background */ +body { + background-color: #e6f2ff; +} + +/* table */ +table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + border-radius: 10px; /* rounded borders */ + background-color: #ffffff; + overflow: hidden; /* hide overflow from rounded borders */ +} + +th { + padding: 12px 15px; + background-color: #f2f2f2; + border-bottom: 1px solid transparent; /* transparent border for rows */ + //border-top-left-radius: 10px; /* rounded top-left corner */ + //border-top-right-radius: 10px; /* rounded top-right corner */ +} + +td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid transparent; /* transparent border for rows */ +} + +/* highlight on hover for table rows */ +tbody tr:hover { + background-color: #f5f5f5; +} + +/* Pagination */ +.pagination { + margin-top: 20px; + display: flex; + align-items: center; /* align items vertically */ +} + +.pagination button { + padding: 8px 12px; + border: 1px solid #007bff; + background-color: #007bff; + color: white; + cursor: pointer; + border-radius: 5px; +} + +.pagination .spacer { + flex-grow: 1; /* grow to fill available space */ +} + +.page-counter { + margin: 0 10px; /* add margin to separate from buttons */ +} + +/* hover effect for buttons */ +.pagination button:hover { + background-color: #0056b3; +} + +/* disabled state for buttons */ +.pagination button:disabled { + background-color: #cccccc; + border-color: #cccccc; + cursor: not-allowed; +} \ No newline at end of file diff --git a/src/router.tsx b/src/router.tsx index 283b71b..9b24d6f 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -7,93 +7,10 @@ import {SampleSidebarHeader} from "./pages/samples/sidebar_header/SampleSidebarH import {SampleModal} from "./pages/samples/modal/SampleModal.tsx"; import GraphDashboard from "./pages/dashboard/DashboardPage.tsx"; import {TestFirestorePage} from "./pages/test firestore/TestFirestore.tsx"; +import {TransactionPage} from "./pages/transactions/TransactionPage.tsx"; // ? Routing - see https://reactrouter.com/en/main -// const tileset_defult: Array<{ text: string; rows: number; cols: number }> = [ -// { text: "Tile 1", cols: 1, rows: 1 }, -// { text: "Tile 2", cols: 1, rows: 1 }, -// { text: "Tile 3", cols: 2, rows: 2 }, -// { text: "Tile 4", cols: 2, rows: 2 }, -// { text: "Tile 5", cols: 1, rows: 1 }, -// { text: "Tile 6", cols: 1, rows: 1 }, -// { text: "Tile 7", cols: 1, rows: 1 }, -// { text: "Tile 8", cols: 1, rows: 1 }, -// { text: "Tile 9", cols: 2, rows: 1 }, -// ]; - -const tileset_column: Array<{ text: string; rows: number; cols: number }> = [ - { text: "Tile 1", cols: 1, rows: 1 }, - { text: "Tile 2", cols: 1, rows: 2 }, - { text: "Tile 3", cols: 2, rows: 2 }, - { text: "Tile 4", cols: 2, rows: 2 }, - { text: "Tile 5", cols: 1, rows: 1 }, - { text: "Tile 6", cols: 1, rows: 1 }, - { text: "Tile 7", cols: 1, rows: 1 }, - { text: "Tile 8", cols: 1, rows: 1 }, - { text: "Tile 9", cols: 2, rows: 1 }, -]; - -const tileset_many: Array<{ text: string; rows: number; cols: number }> = [ - { text: "Tile 1", cols: 1, rows: 1 }, - { text: "Tile 2", cols: 2, rows: 1 }, - { text: "Tile 3", cols: 2, rows: 2 }, - { text: "Tile 4", cols: 2, rows: 2 }, - { text: "Tile 5", cols: 1, rows: 1 }, - { text: "Tile 6", cols: 1, rows: 1 }, - { text: "Tile 7", cols: 1, rows: 1 }, - { text: "Tile 8", cols: 1, rows: 1 }, - { text: "Tile 9", cols: 2, rows: 1 }, - { text: "Tile 11", cols: 1, rows: 1 }, - { text: "Tile 12", cols: 2, rows: 1 }, - { text: "Tile 13", cols: 2, rows: 2 }, - { text: "Tile 14", cols: 2, rows: 2 }, - { text: "Tile 15", cols: 1, rows: 1 }, - {text: "Tile 16", cols: 1, rows:1 }, - { text: "Tile 17", cols: 1, rows: 1 }, - { text: "Tile 18", cols: 1, rows: 1 }, - { text: "Tile 19", cols: 2, rows: 1 }, - { text: "Tile 21", cols: 1, rows: 1 }, - { text: "Tile 22", cols: 2, rows: 1 }, - { text: "Tile 23", cols: 2, rows: 2 }, - { text: "Tile 24", cols: 2, rows: 2 }, - { text: "Tile 25", cols: 1, rows: 1 }, - { text: "Tile 26", cols: 1, rows: 1 }, - { text: "Tile 27", cols: 1, rows: 1 }, - { text: "Tile 28", cols: 1, rows: 1 }, - { text: "Tile 29", cols: 2, rows: 1 }, - { text: "Tile 31", cols: 1, rows: 1 }, - { text: "Tile 32", cols: 2, rows: 1 }, - { text: "Tile 33", cols: 2, rows: 2 }, - { text: "Tile 34", cols: 2, rows: 2 }, - { text: "Tile 35", cols: 1, rows: 1 }, - { text: "Tile 36", cols: 1, rows: 1 }, - { text: "Tile 37", cols: 1, rows: 1 }, - { text: "Tile 38", cols: 1, rows: 1 }, - { text: "Tile 39", cols: 2, rows: 1 }, -]; - -const tileset_weird: Array<{ text: string; rows: number; cols: number }> = [ - { text: "Tile 1", cols: 1, rows: 3 }, - { text: "Tile 2", cols: 2, rows: 3 }, - { text: "Tile 3", cols: 3, rows: 2 }, - { text: "Tile 4", cols: 3, rows: 2 }, - { text: "Tile 5", cols: 3, rows: 1 }, - { text: "Tile 6", cols: 1, rows: 1 }, - { text: "Tile 7", cols: 1, rows: 1 }, - { text: "Tile 8", cols: 1, rows: 1 }, - { text: "Tile 9", cols: 2, rows: 1 }, - { text: "Tile 11", cols: 1, rows: 1 }, - { text: "Tile 12", cols: 2, rows: 1 }, - { text: "Tile 13", cols: 2, rows: 2 }, - { text: "Tile 14", cols: 2, rows: 2 }, - { text: "Tile 15", cols: 1, rows: 1 }, - { text: "Tile 16", cols: 1, rows: 1 }, - { text: "Tile 17", cols: 3, rows: 3 }, - { text: "Tile 18", cols: 1, rows: 1 }, - { text: "Tile 19", cols: 2, rows: 1 }, -]; - export const router = createBrowserRouter([ { path: "/", @@ -102,15 +19,12 @@ export const router = createBrowserRouter([ }, { path: "/dash", - element: , + element: , }, { path: "/transactions", - element: , + element: , }, - // --> - - { path: "/user-test", element: , @@ -120,18 +34,6 @@ export const router = createBrowserRouter([ path: "/graphs", element: , }, - { - path: "/dash-2", - element: , - }, - { - path: "/dash-3", - element: , - }, - { - path: "/dash-4", - element: , - }, { path: "/sample_sidebar", element: , diff --git a/src/utils/transaction_utils.ts b/src/utils/transaction_utils.ts index 53dd236..69fa01d 100644 --- a/src/utils/transaction_utils.ts +++ b/src/utils/transaction_utils.ts @@ -1,6 +1,6 @@ import {User} from "firebase/auth"; import {getTransactions, getTransactionsFilterOrderBy, Transaction} from "./transaction.ts"; -import {limit, startAfter, where} from "firebase/firestore"; +import {where} from "firebase/firestore"; export async function getCurrentBalance(user: User): Promise { const transactions = await getTransactions(user); @@ -24,9 +24,4 @@ const DAY_MILLIS = 8.64e+7; export async function getLastDayTransaction(user: User): Promise { return await getTransactionsFilterOrderBy(user, where("dateTime", ">", Date.now() - DAY_MILLIS)); -} - -// Returns `pageSize` transactions for the given `user` with the `docName` attribute set -export async function getTransactionsPage(user: User, pageSize: number, page: number): Promise { - return await getTransactionsFilterOrderBy(user, startAfter(page * pageSize), limit(pageSize)); } \ No newline at end of file