From 7e6f8ce681e08bf77d303a7b0488332c5aec285c Mon Sep 17 00:00:00 2001 From: joesumargo Date: Fri, 26 Apr 2024 22:20:38 +0100 Subject: [PATCH 01/10] Added connection to firebase but --- src/pages/dashboard/DashboardPage.tsx | 59 ++++++--------------------- src/pages/dashboard/GraphUtils.ts | 19 +++++---- 2 files changed, 23 insertions(+), 55 deletions(-) diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx index 03e5549..e54078b 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/pages/dashboard/DashboardPage.tsx @@ -1,13 +1,14 @@ import "./styles.css"; import "react-tiles-dnd/esm/index.css"; -import { TilesContainer, RenderTileFunction } from "react-tiles-dnd"; +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 { getTransactionsFilterOrderBy, Transaction } from "../../utils/transaction.ts" +import {Header} from "../../components/Header.tsx"; +import {getTransactionsFilterOrderBy, Transaction} from "../../utils/transaction.ts" +import {getCurrentBalance} from "../../utils/transaction_utils.ts"; +import {readTransactions} from "./GraphUtils.ts"; import {auth} from "../../utils/firebase.ts"; import {orderBy} from "firebase/firestore"; -import {getCurrentBalance} from "../../utils/transaction_utils.ts"; import { User } from "firebase/auth"; import {FullscreenCenter} from "../../components/FullscreenCenter.tsx"; import {Button} from "react-bootstrap"; @@ -55,48 +56,10 @@ export default function Dashboard() { const [showCSVModal, setShowCSVModal] = useState(false); const [showTransactionModal, setShowTransactionModal] = useState(false); - const tileSize = (tile: typeof transactionTiles[0]) => ({ - colSpan: tile.cols, - rowSpan: tile.rows - }); - 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 - const month = (date.getMonth() + 1).toString().padStart(2, '0'); // Month is 0-indexed, add 1 - const year = date.getFullYear(); - return `${day}/${month}/${year}`; - } - const splitTransactions = (data: transactionPoint[]): void => { - const moneyIn: transactionPoint[] = [] - const moneyOut: transactionPoint[] = [] - data.forEach(t => { - if (t.amount > 0) { - moneyIn.push(t) - } else { - moneyOut.push(t) - } - }) - setPoints([cumulateTransactions(data), cumulateTransactions(moneyIn), cumulateTransactions(moneyOut)]) - console.log(transactionPoints) - } - const readTransactions = (data: Transaction[]): void => { - const result: transactionPoint[] = [] - data.forEach(t => { - result.push({amount: t.amount, date: getDateString(t.dateTime)}) - }) - splitTransactions(result) - } const fetchTransactions = async (user: User) => { try { - const transactions = await getTransactionsFilterOrderBy(user, orderBy("dateTime", "desc")) - readTransactions(transactions) + const transactions = await getTransactionsFilterOrderBy(user, orderBy("dateTime", "asc")) + await readTransactions(transactions).then((t) => setPoints(t)) setFetchResolved(true); } catch (error) {} } @@ -112,7 +75,7 @@ export default function Dashboard() { } },[auth.currentUser]) - + // Forces user to wait till everything has loaded if (!authResolved) { auth.authStateReady().then(() => setAuthResolved(true)); return <> @@ -134,13 +97,17 @@ export default function Dashboard() { ; } + // Tiles + const tileSize = (tile: typeof transactionTiles[0]) => ({ + colSpan: tile.cols, + rowSpan: tile.rows + }); const transactionTiles = [ {d: TileElement.newGraph(transactionPoints[0]), cols:5, rows:2}, {d: TileElement.newGraph(transactionPoints[1]), cols:5, rows:2}, {d: TileElement.newGraph(transactionPoints[2]), cols:5, rows:2}, {d: TileElement.newTSX(test), cols:1, rows:1} ]; - const renderFirebase: RenderTileFunction = ({ data, isDragging }) => (
{ +export const cumulateTransactions = (points: transactionPoint[]): transactionPoint[] => { let total = 0; return points.map(value => { total += value.amount; @@ -16,7 +16,14 @@ const getDateString = (timestamp: number): string => { const year = date.getFullYear(); return `${day}/${month}/${year}`; } -export const splitTransactions = (data: transactionPoint[]): void => { +export const readTransactions = async (data: Transaction[]): Promise => { + const result: transactionPoint[] = [] + data.forEach(t => { + result.push({amount: t.amount, date: getDateString(t.dateTime)}) + }) + return splitTransactions(result) +} +export const splitTransactions = (data: transactionPoint[]): transactionPoint[][] => { const moneyIn: transactionPoint[] = [] const moneyOut: transactionPoint[] = [] data.forEach(t => { @@ -26,11 +33,5 @@ export const splitTransactions = (data: transactionPoint[]): void => { moneyOut.push(t) } }) -} -export const readTransactions = (data: Transaction[]): void => { - const result: transactionPoint[] = [] - data.forEach(t => { - result.push({amount: t.amount, date: getDateString(t.dateTime)}) - }) - splitTransactions(result) + return ([cumulateTransactions(data), cumulateTransactions(moneyIn), cumulateTransactions(moneyOut)]) } \ No newline at end of file From 7a9b4dea68fa6baa1bad152d14f74af87090b2e5 Mon Sep 17 00:00:00 2001 From: Robert Lucas <100799838+Robert-M-Lucas@users.noreply.github.com> Date: Sat, 27 Apr 2024 18:52:29 +0100 Subject: [PATCH 02/10] Separated DashboardPage.tsx functionality into multiple files --- src/pages/dashboard/DashboardPage.tsx | 92 ++++----------------------- src/pages/dashboard/GraphUtils.ts | 44 +++++++------ src/pages/dashboard/Graphs.tsx | 3 +- src/pages/dashboard/TileUtils.ts | 32 ++++++++++ 4 files changed, 69 insertions(+), 102 deletions(-) create mode 100644 src/pages/dashboard/TileUtils.ts diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx index f8b4c78..b524d0b 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/pages/dashboard/DashboardPage.tsx @@ -3,8 +3,8 @@ 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 {ReactNode, useEffect, useState} from "react"; -import { getTransactionsFilterOrderBy, Transaction } from "../../utils/transaction.ts" +import {useEffect, useState} from "react"; +import { getTransactionsFilterOrderBy } from "../../utils/transaction.ts" import {auth} from "../../utils/firebase.ts"; import {orderBy} from "firebase/firestore"; import {getCurrentBalance} from "../../utils/transaction_utils.ts"; @@ -15,43 +15,13 @@ import {CSVUpload} from "../../components/transactions/CSVUpload.tsx"; import {InputTransaction} from "../../components/transactions/InputTransaction.tsx"; import Graphs from "./Graphs.tsx" import test from "./test.tsx" - -type transactionPoint = { date: string; amount: number } -type tsxContents = ReactNode; - -class TileElement { - private graph?: transactionPoint[]; - private TSX?: () => tsxContents; - - constructor(graph: transactionPoint[] | undefined, TSX: (() => tsxContents) | undefined) { - this.graph = graph; - this.TSX = TSX; - } - - static newGraph(graph: transactionPoint[]): TileElement { - return new TileElement(graph, undefined); - } - static newTSX(TSX: () => tsxContents): TileElement { - return new TileElement(undefined, TSX); - } - - isGraph(): boolean { - return typeof this.graph !== "undefined"; - } - - forceGetGraph(): transactionPoint[] { - return this.graph!; - } - forceGetTSX(): () => tsxContents { - return this.TSX!; - } -} +import {TileElement } from "./TileUtils.ts"; +import {finalGraphData, readTransactions} from "./GraphUtils.ts"; export default function Dashboard() { const [balance, setBalance] = useState(0); - const [transactionPoints, setPoints] = useState([[]]); + const [transactionPoints, setPoints] = useState(null); const [authResolved, setAuthResolved] = useState(false); - const [fetchResolved, setFetchResolved] = useState(false); const [showCSVModal, setShowCSVModal] = useState(false); const [showTransactionModal, setShowTransactionModal] = useState(false); @@ -59,46 +29,10 @@ export default function Dashboard() { colSpan: tile.cols, rowSpan: tile.rows }); - 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 - const month = (date.getMonth() + 1).toString().padStart(2, '0'); // Month is 0-indexed, add 1 - const year = date.getFullYear(); - return `${day}/${month}/${year}`; - } - const splitTransactions = (data: transactionPoint[]): void => { - const moneyIn: transactionPoint[] = [] - const moneyOut: transactionPoint[] = [] - data.forEach(t => { - if (t.amount > 0) { - moneyIn.push(t) - } else { - moneyOut.push(t) - } - }) - setPoints([cumulateTransactions(data), cumulateTransactions(moneyIn), cumulateTransactions(moneyOut)]) - console.log(transactionPoints) - } - const readTransactions = (data: Transaction[]): void => { - const result: transactionPoint[] = [] - data.forEach(t => { - result.push({amount: t.amount, date: getDateString(t.dateTime)}) - }) - splitTransactions(result) - } + const fetchTransactions = async (user: User) => { - try { - const transactions = await getTransactionsFilterOrderBy(user, orderBy("dateTime", "desc")) - readTransactions(transactions) - setFetchResolved(true); - } catch (error) {} + const transactions = await getTransactionsFilterOrderBy(user, orderBy("dateTime", "desc")) + setPoints(readTransactions(transactions)); } const {width} = useWindowDimensions(); @@ -123,8 +57,8 @@ export default function Dashboard() { ; } - if (!fetchResolved) { - auth.authStateReady().then(() => setAuthResolved(true)); + + if (!transactionPoints) { return <>
@@ -135,9 +69,9 @@ export default function Dashboard() { } const transactionTiles = [ - {d: TileElement.newGraph(transactionPoints[0]), cols:5, rows:2}, - {d: TileElement.newGraph(transactionPoints[1]), cols:5, rows:2}, - {d: TileElement.newGraph(transactionPoints[2]), cols:5, rows:2}, + {d: TileElement.newGraph(transactionPoints.raw), cols:5, rows:2}, + {d: TileElement.newGraph(transactionPoints.in), cols:5, rows:2}, + {d: TileElement.newGraph(transactionPoints.out), cols:5, rows:2}, {d: TileElement.newTSX(test), cols:1, rows:1} ]; diff --git a/src/pages/dashboard/GraphUtils.ts b/src/pages/dashboard/GraphUtils.ts index 266777c..cad8f00 100644 --- a/src/pages/dashboard/GraphUtils.ts +++ b/src/pages/dashboard/GraphUtils.ts @@ -1,23 +1,23 @@ import {Transaction} from "../../utils/transaction.ts"; +import strftime from "strftime"; -type transactionPoint = { date: string; amount: number } +export type transactionPoint = { date: string; amount: number } +export type finalGraphData = {raw: transactionPoint[], in: transactionPoint[], out: transactionPoint[]}; -// const cumulateTransactions = (points: transactionPoint[]): transactionPoint[] => { -// let total = 0; -// return points.map(value => { -// total += value.amount; -// return {date: value.date, amount: total}; -// }) -// } +function cumulateTransactions(points: transactionPoint[]): transactionPoint[] { + let total = 0; + return points.map(value => { + total += value.amount; + value.amount = total; + return value; + }) +} -const getDateString = (timestamp: number): string => { - const date = new Date(timestamp) - const day = date.getDate().toString().padStart(2, '0'); // Ensures two digits - const month = (date.getMonth() + 1).toString().padStart(2, '0'); // Month is 0-indexed, add 1 - const year = date.getFullYear(); - return `${day}/${month}/${year}`; +function getDateString(timestamp: number): string { + return strftime("%d/%m/%y", new Date(timestamp)) } -export const splitTransactions = (data: transactionPoint[]): void => { + +function splitTransactions (data: transactionPoint[]): finalGraphData { const moneyIn: transactionPoint[] = [] const moneyOut: transactionPoint[] = [] data.forEach(t => { @@ -27,11 +27,13 @@ export const splitTransactions = (data: transactionPoint[]): void => { moneyOut.push(t) } }) + return {raw: cumulateTransactions(data), in: cumulateTransactions(moneyIn), out: cumulateTransactions(moneyOut)}; } -export const readTransactions = (data: Transaction[]): void => { - const result: transactionPoint[] = [] - data.forEach(t => { - result.push({amount: t.amount, date: getDateString(t.dateTime)}) - }) - splitTransactions(result) + +export function readTransactions(data: Transaction[]): finalGraphData { + return splitTransactions( + data.map((t) => { + return {date: getDateString(t.dateTime), amount: t.amount} + }) + ); } \ No newline at end of file diff --git a/src/pages/dashboard/Graphs.tsx b/src/pages/dashboard/Graphs.tsx index 8bf93e7..5b1cdc5 100644 --- a/src/pages/dashboard/Graphs.tsx +++ b/src/pages/dashboard/Graphs.tsx @@ -1,6 +1,5 @@ import {Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis} from "recharts"; - -type transactionPoint = { date: string; amount: number } +import {transactionPoint} from "./GraphUtils.ts"; interface Props { data: transactionPoint[]; diff --git a/src/pages/dashboard/TileUtils.ts b/src/pages/dashboard/TileUtils.ts new file mode 100644 index 0000000..792d6ad --- /dev/null +++ b/src/pages/dashboard/TileUtils.ts @@ -0,0 +1,32 @@ +import {ReactNode} from "react"; +import {transactionPoint} from "./GraphUtils.ts"; + +type tsxContents = ReactNode; + +export class TileElement { + private readonly graph?: transactionPoint[]; + private readonly TSX?: () => tsxContents; + + constructor(graph: transactionPoint[] | undefined, TSX: (() => tsxContents) | undefined) { + this.graph = graph; + this.TSX = TSX; + } + + static newGraph(graph: transactionPoint[]): TileElement { + return new TileElement(graph, undefined); + } + static newTSX(TSX: () => tsxContents): TileElement { + return new TileElement(undefined, TSX); + } + + isGraph(): boolean { + return typeof this.graph !== "undefined"; + } + + forceGetGraph(): transactionPoint[] { + return this.graph!; + } + forceGetTSX(): () => tsxContents { + return this.TSX!; + } +} \ No newline at end of file From 94ec068bf7ae61406d793ef6177ed7b091ac36ab Mon Sep 17 00:00:00 2001 From: Robert Lucas <100799838+Robert-M-Lucas@users.noreply.github.com> Date: Sat, 27 Apr 2024 19:15:19 +0100 Subject: [PATCH 03/10] Continued rework of tile interface --- README.md | 4 +- src/components/Header.tsx | 2 +- src/pages/dashboard/DashboardPage.tsx | 92 +++++++++++++++------------ src/pages/dashboard/TileUtils.ts | 21 ++++-- src/pages/dashboard/Tiles.tsx | 8 +++ src/pages/dashboard/test.tsx | 7 -- src/router.tsx | 4 +- 7 files changed, 81 insertions(+), 57 deletions(-) create mode 100644 src/pages/dashboard/Tiles.tsx delete mode 100644 src/pages/dashboard/test.tsx diff --git a/README.md b/README.md index 8e2693d..430c656 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Start Firebase emulator(s): `firebase emulators:start` ### Test Deployment -Creating a test deployment to Firebase: +Creating a tiles deployment to Firebase: ``` npm run build firebase hosting:channel:deploy [Test Deployment Name] @@ -51,7 +51,7 @@ firebase hosting:channel:deploy [Test Deployment Name] ### Production Deployment Making a [pull request](https://github.com/Robert-M-Lucas/budget-19/compare) -will automatically create a test deployment to Firebase (i.e. not +will automatically create a tiles deployment to Firebase (i.e. not overwrite the production app). You should see a message below your new pull request notifying you that your changes are being deployed. Click on the link shown when this is complete to view your changes. The pull diff --git a/src/components/Header.tsx b/src/components/Header.tsx index fd668e9..ec298ff 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -42,7 +42,7 @@ export function Header() { {/* Links to transactions page with table of expenses */}
  • Transactions
  • -
  • Firestore Test
  • +
  • Firestore Test
  • {displayName &&
  • {displayName}
  • } diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx index b524d0b..b07d7d8 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/pages/dashboard/DashboardPage.tsx @@ -4,52 +4,46 @@ import { TilesContainer, RenderTileFunction } from "react-tiles-dnd"; import useWindowDimensions from "../../hooks/WindowDimensionsHook.tsx"; import {Header} from "../../components/Header.tsx"; import {useEffect, useState} from "react"; -import { getTransactionsFilterOrderBy } from "../../utils/transaction.ts" +import {Transaction, getTransactionsFilterOrderBy } from "../../utils/transaction.ts" import {auth} from "../../utils/firebase.ts"; import {orderBy} from "firebase/firestore"; -import {getCurrentBalance} from "../../utils/transaction_utils.ts"; import { User } from "firebase/auth"; import {FullscreenCenter} from "../../components/FullscreenCenter.tsx"; -import {Button} from "react-bootstrap"; -import {CSVUpload} from "../../components/transactions/CSVUpload.tsx"; -import {InputTransaction} from "../../components/transactions/InputTransaction.tsx"; import Graphs from "./Graphs.tsx" -import test from "./test.tsx" -import {TileElement } from "./TileUtils.ts"; +import {getTileSize, TileElement} from "./TileUtils.ts"; import {finalGraphData, readTransactions} from "./GraphUtils.ts"; +import {signInWithGoogle} from "../../utils/authentication.ts"; +import totalTile from "./Tiles.tsx"; export default function Dashboard() { - const [balance, setBalance] = useState(0); + // const [balance, setBalance] = useState(0); const [transactionPoints, setPoints] = useState(null); + const [transactions, setTransactions] = useState([]); const [authResolved, setAuthResolved] = useState(false); - const [showCSVModal, setShowCSVModal] = useState(false); - const [showTransactionModal, setShowTransactionModal] = useState(false); - - const tileSize = (tile: typeof transactionTiles[0]) => ({ - colSpan: tile.cols, - rowSpan: tile.rows - }); + // const [showCSVModal, setShowCSVModal] = useState(false); + // const [showTransactionModal, setShowTransactionModal] = useState(false); + const [update, setUpdate] = useState(0) const fetchTransactions = async (user: User) => { - const transactions = await getTransactionsFilterOrderBy(user, orderBy("dateTime", "desc")) + const transactions = await getTransactionsFilterOrderBy(user, orderBy("dateTime", "desc")); + setTransactions(transactions); setPoints(readTransactions(transactions)); } const {width} = useWindowDimensions(); - const columns = Math.max(Math.floor(width / 200), 1); + const columns = Math.max(Math.floor(width / 180), 1); // Transaction Loading and Handling useEffect(() => { if (auth.currentUser !== null) { - getCurrentBalance(auth.currentUser).then((b) => setBalance(b)); - fetchTransactions(auth.currentUser).then(() => console.log("Fetched Transactions", transactionPoints)); + fetchTransactions(auth.currentUser).then(); } },[auth.currentUser]) - if (!authResolved) { auth.authStateReady().then(() => setAuthResolved(true)); return <> +

    Waiting for Auth

    @@ -58,8 +52,26 @@ export default function Dashboard() { ; } + if (auth.currentUser === null) { + auth.onAuthStateChanged(() => { + setUpdate(update + 1); + }); + return <> +
    + +
    +

    Not Logged In

    + +
    +
    + ; + } + if (!transactionPoints) { return <> +

    Fetching

    @@ -68,18 +80,18 @@ export default function Dashboard() { ; } - const transactionTiles = [ - {d: TileElement.newGraph(transactionPoints.raw), cols:5, rows:2}, - {d: TileElement.newGraph(transactionPoints.in), cols:5, rows:2}, - {d: TileElement.newGraph(transactionPoints.out), cols:5, rows:2}, - {d: TileElement.newTSX(test), cols:1, rows:1} + const transactionTiles: TileElement[] = [ + TileElement.newTSX(() => totalTile(transactions), 1, 1, columns), + TileElement.newGraph(transactionPoints.raw, 3, 2, columns), + TileElement.newGraph(transactionPoints.in, 3, 2, columns), + TileElement.newGraph(transactionPoints.out, 3, 2, columns), ]; - const renderFirebase: RenderTileFunction = ({ data, isDragging }) => ( + const renderTile: RenderTileFunction = ({ data, isDragging }) => (
    - {data.d.isGraph() ? : data.d.forceGetTSX()()} + {data.isGraph() ? : data.forceGetTSX()()}
    ); @@ -87,23 +99,23 @@ export default function Dashboard() { return (
    -
    - - + {/*
    */} + {/* */} + {/* */} - - -
    + {/* */} + {/* */} + {/*
    */}
    - - {balance} + {/**/} + {/*{balance}*/} diff --git a/src/pages/dashboard/TileUtils.ts b/src/pages/dashboard/TileUtils.ts index 792d6ad..3fbee2b 100644 --- a/src/pages/dashboard/TileUtils.ts +++ b/src/pages/dashboard/TileUtils.ts @@ -1,22 +1,27 @@ import {ReactNode} from "react"; import {transactionPoint} from "./GraphUtils.ts"; +import {min} from "lodash"; type tsxContents = ReactNode; export class TileElement { private readonly graph?: transactionPoint[]; private readonly TSX?: () => tsxContents; + public readonly cols: number; + public readonly rows: number; - constructor(graph: transactionPoint[] | undefined, TSX: (() => tsxContents) | undefined) { + private constructor(graph: transactionPoint[] | undefined, TSX: (() => tsxContents) | undefined, cols: number, rows: number) { this.graph = graph; this.TSX = TSX; + this.cols = cols; + this.rows = rows; } - static newGraph(graph: transactionPoint[]): TileElement { - return new TileElement(graph, undefined); + static newGraph(graph: transactionPoint[], cols: number, rows: number, maxCol: number): TileElement { + return new TileElement(graph, undefined, min([cols, maxCol])!, rows); } - static newTSX(TSX: () => tsxContents): TileElement { - return new TileElement(undefined, TSX); + static newTSX(TSX: () => tsxContents, cols: number, rows: number, maxCol: number): TileElement { + return new TileElement(undefined, TSX, min([cols, maxCol])!, rows); } isGraph(): boolean { @@ -29,4 +34,10 @@ export class TileElement { forceGetTSX(): () => tsxContents { return this.TSX!; } +} + +export type tileData = { d: TileElement; rows: number; cols: number }; + +export function getTileSize (tile: TileElement) { + return {colSpan: tile.cols, rowSpan: tile.rows}; } \ No newline at end of file diff --git a/src/pages/dashboard/Tiles.tsx b/src/pages/dashboard/Tiles.tsx new file mode 100644 index 0000000..0155cff --- /dev/null +++ b/src/pages/dashboard/Tiles.tsx @@ -0,0 +1,8 @@ +import {ReactNode} from "react"; +import { Transaction } from "../../utils/transaction"; + +export default function totalTile(transactions: Transaction[]): ReactNode { + return <> +

    Total: {transactions.reduce((prev, curr): number => prev + curr.amount, 0).toFixed(2)}

    + ; +} \ No newline at end of file diff --git a/src/pages/dashboard/test.tsx b/src/pages/dashboard/test.tsx deleted file mode 100644 index 13cba03..0000000 --- a/src/pages/dashboard/test.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function test() { - return ( - <> - Hi - - ); -} \ No newline at end of file diff --git a/src/router.tsx b/src/router.tsx index 9b24d6f..f5b2c09 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -26,7 +26,7 @@ export const router = createBrowserRouter([ element: , }, { - path: "/user-test", + path: "/user-tiles", element: , errorElement:<_404Page/> }, @@ -47,7 +47,7 @@ export const router = createBrowserRouter([ element: , }, { - path: "/test", + path: "/tiles", element: , }, ]); \ No newline at end of file From 6e5d46b35c529f4e80127d148f3e62a86a4386cb Mon Sep 17 00:00:00 2001 From: Robert Lucas <100799838+Robert-M-Lucas@users.noreply.github.com> Date: Sat, 27 Apr 2024 19:43:57 +0100 Subject: [PATCH 04/10] Added total tile --- src/pages/dashboard/DashboardPage.tsx | 2 +- src/pages/dashboard/Tiles.tsx | 36 ++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx index b07d7d8..7be0ea7 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/pages/dashboard/DashboardPage.tsx @@ -81,7 +81,7 @@ export default function Dashboard() { } const transactionTiles: TileElement[] = [ - TileElement.newTSX(() => totalTile(transactions), 1, 1, columns), + TileElement.newTSX(() => totalTile(transactions), 2, 1, columns), TileElement.newGraph(transactionPoints.raw, 3, 2, columns), TileElement.newGraph(transactionPoints.in, 3, 2, columns), TileElement.newGraph(transactionPoints.out, 3, 2, columns), diff --git a/src/pages/dashboard/Tiles.tsx b/src/pages/dashboard/Tiles.tsx index 0155cff..6bad195 100644 --- a/src/pages/dashboard/Tiles.tsx +++ b/src/pages/dashboard/Tiles.tsx @@ -1,8 +1,38 @@ import {ReactNode} from "react"; import { Transaction } from "../../utils/transaction"; +import {max, min} from "lodash"; export default function totalTile(transactions: Transaction[]): ReactNode { - return <> -

    Total: {transactions.reduce((prev, curr): number => prev + curr.amount, 0).toFixed(2)}

    - ; + const balance = transactions.reduce((prev, curr): number => prev + curr.amount, 0); + const income = transactions.reduce((prev, curr): number => prev + max([curr.amount, 0])!, 0); + const expenses = transactions.reduce((prev, curr): number => prev + min([curr.amount, 0])!, 0); + + return
      +
    • +
      +
      Balance:
      + {balance > 0 ? <> +
      £
      +
      {balance.toFixed(2)}
      + : <> +
      £
      +
      {balance.toFixed(2)}
      + } +
      +
    • +
    • +
      +
      Income:
      +
      £
      +
      {income.toFixed(2)}
      +
      +
    • +
    • +
      +
      Expenses:
      +
      £
      +
      {expenses.toFixed(2)}
      +
      +
    • +
    ; } \ No newline at end of file From f08ffbc080d53defe97d2339ab8f0f1a02ac845a Mon Sep 17 00:00:00 2001 From: Robert Lucas <100799838+Robert-M-Lucas@users.noreply.github.com> Date: Sat, 27 Apr 2024 21:00:02 +0100 Subject: [PATCH 05/10] Added final UserPrefs --- src/pages/test firestore/TestFirestore.tsx | 12 ++++-- src/utils/user_prefs.ts | 46 +++++++++++++++++++--- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/pages/test firestore/TestFirestore.tsx b/src/pages/test firestore/TestFirestore.tsx index 2655ca2..899c941 100644 --- a/src/pages/test firestore/TestFirestore.tsx +++ b/src/pages/test firestore/TestFirestore.tsx @@ -15,6 +15,7 @@ import {Header} from "../../components/Header.tsx"; import { orderBy } from "firebase/firestore"; import {User} from "firebase/auth"; import {getUserPrefs, setUserPrefs, UserPrefs} from "../../utils/user_prefs.ts"; +import {round} from "lodash"; function writeSampleData() { if (auth.currentUser === null) { @@ -95,11 +96,14 @@ export function TestFirestorePage() {

    UserPrefs

    { userPrefs ? <> -

    Goal: {userPrefs.goal}

    +

    Goal: {userPrefs.getNeedsBudget()} | {userPrefs.getWantsBudget()} | {userPrefs.getSavingsBudget()}

    :

    Loading

    } diff --git a/src/utils/user_prefs.ts b/src/utils/user_prefs.ts index 2aa65b5..24ebba6 100644 --- a/src/utils/user_prefs.ts +++ b/src/utils/user_prefs.ts @@ -1,17 +1,52 @@ import {collection, doc, DocumentSnapshot, getDoc, setDoc, SnapshotOptions} from "firebase/firestore"; import {User} from "firebase/auth"; import {db} from "./firebase.ts"; +import {round} from "lodash"; export class UserPrefs { - public goal: number; + private readonly needsBudget: number; + private readonly wantsBudget: number; - constructor(goal: number) { - this.goal = goal; + private constructor(needsBudget: number, wantsBudget: number) { + if (needsBudget > 1) { + needsBudget = 1; + } + + if (needsBudget + wantsBudget > 1) { + wantsBudget = 1 - needsBudget; + } + + this.needsBudget = round(needsBudget, 2); + this.wantsBudget = round(wantsBudget, 2); + } + + static newChecked(needsBudget: number, wantsBudget: number): UserPrefs | Error { + if (needsBudget > 1) { + return new Error("needsBudget > 1!"); + } + + if (needsBudget + wantsBudget > 1) { + return new Error("needsBudget + wantsBudget > 1!"); + } + + return new UserPrefs(needsBudget, wantsBudget); } static default(): UserPrefs { - return new UserPrefs(100); + return new UserPrefs(0.5, 0.3); + } + + getNeedsBudget(): number { + return this.needsBudget; + } + + getWantsBudget(): number { + return this.wantsBudget; + } + + getSavingsBudget(): number { + return round(1 - this.wantsBudget - this.needsBudget, 2); } // Utility method for creating `Transactions` @@ -20,7 +55,8 @@ export class UserPrefs { if (!data) { throw Error("No data returned for snapshot!"); } - return new UserPrefs(data.goal); + + return new UserPrefs(round(data.needsBudget, 2), round(data.wantsBudget, 2)); } toSendObject(): object { From f9c1a1db52363462caf68a834ca35f0fca7edd73 Mon Sep 17 00:00:00 2001 From: joesumargo Date: Sat, 27 Apr 2024 21:09:07 +0100 Subject: [PATCH 06/10] Added in user prefs into DashboardPage.tsx --- src/pages/dashboard/DashboardPage.tsx | 47 ++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx index e54078b..fd2714c 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/pages/dashboard/DashboardPage.tsx @@ -2,10 +2,11 @@ import "./styles.css"; import "react-tiles-dnd/esm/index.css"; import {TilesContainer, RenderTileFunction} from "react-tiles-dnd"; import useWindowDimensions from "../../hooks/WindowDimensionsHook.tsx"; -import React, {ReactNode, useEffect, useState} from "react"; +import {ReactNode, useEffect, useState} from "react"; import {Header} from "../../components/Header.tsx"; -import {getTransactionsFilterOrderBy, Transaction} from "../../utils/transaction.ts" +import {getTransactionsFilterOrderBy} from "../../utils/transaction.ts" import {getCurrentBalance} from "../../utils/transaction_utils.ts"; +import {getUserPrefs, UserPrefs} from "../../utils/user_prefs.ts"; import {readTransactions} from "./GraphUtils.ts"; import {auth} from "../../utils/firebase.ts"; import {orderBy} from "firebase/firestore"; @@ -51,17 +52,29 @@ class TileElement { export default function Dashboard() { const [balance, setBalance] = useState(0); const [transactionPoints, setPoints] = useState([[]]); + const [userResolved, setUserResolved] = useState(false); const [authResolved, setAuthResolved] = useState(false); const [fetchResolved, setFetchResolved] = useState(false); const [showCSVModal, setShowCSVModal] = useState(false); const [showTransactionModal, setShowTransactionModal] = useState(false); - const fetchTransactions = async (user: User) => { + const fetchTransactions = async (user: User, goal: number) => { try { const transactions = await getTransactionsFilterOrderBy(user, orderBy("dateTime", "asc")) - await readTransactions(transactions).then((t) => setPoints(t)) + console.log("Check 0", transactions) + await readTransactions(transactions, goal).then((t) => { + console.log("Check 1", t) + setPoints(t) + }) setFetchResolved(true); - } catch (error) {} + } catch (error) { console.error("Error in fetchTransactions", error) } + } + const fetchUserPrefs = async (user: User): Promise => { + try { + const u = await getUserPrefs(user); + setUserResolved(true); + return u + } catch (error) { console.error("Error in fetchUserPrefs", error) } } const {width} = useWindowDimensions(); @@ -71,7 +84,15 @@ export default function Dashboard() { useEffect(() => { if (auth.currentUser !== null) { getCurrentBalance(auth.currentUser).then((b) => setBalance(b)); - fetchTransactions(auth.currentUser).then(() => console.log("Fetched Transactions", transactionPoints)); + fetchUserPrefs(auth.currentUser).then((u) => { + console.log("Fetched UserPrefs", u) + if (u) { + fetchTransactions(auth.currentUser!, u!.goal).then(() => { + console.log("Fetched Transactions", transactionPoints) + }); + } + }) + } },[auth.currentUser]) @@ -81,17 +102,25 @@ export default function Dashboard() { return <>
    -

    Waiting for Auth

    +

    Waiting for Authentication

    +
    +
    + ; + } + if (!userResolved) { + return <> + +
    +

    Fetching User Data

    ; } if (!fetchResolved) { - auth.authStateReady().then(() => setAuthResolved(true)); return <>
    -

    Fetching

    +

    Fetching User Transactions

    ; From ccafd9c5e050387c8ac1dc6766fd0ba030e06de8 Mon Sep 17 00:00:00 2001 From: joesumargo Date: Sat, 27 Apr 2024 21:12:20 +0100 Subject: [PATCH 07/10] Added in user prefs into DashboardPage.tsx --- src/pages/dashboard/GraphUtils.ts | 12 ++++++++---- src/pages/dashboard/Graphs.tsx | 6 +++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/pages/dashboard/GraphUtils.ts b/src/pages/dashboard/GraphUtils.ts index 9498bc0..75fc0e2 100644 --- a/src/pages/dashboard/GraphUtils.ts +++ b/src/pages/dashboard/GraphUtils.ts @@ -1,12 +1,12 @@ import {Transaction} from "../../utils/transaction.ts"; -type transactionPoint = { date: string; amount: number } +type transactionPoint = { date: string; amount: number; goal: number } export const cumulateTransactions = (points: transactionPoint[]): transactionPoint[] => { let total = 0; return points.map(value => { total += value.amount; - return {date: value.date, amount: total}; + return {date: value.date, amount: total, goal: value.goal}; }) } const getDateString = (timestamp: number): string => { @@ -16,11 +16,15 @@ const getDateString = (timestamp: number): string => { const year = date.getFullYear(); return `${day}/${month}/${year}`; } -export const readTransactions = async (data: Transaction[]): Promise => { +export const readTransactions = async (data: Transaction[], goal: number): Promise => { const result: transactionPoint[] = [] data.forEach(t => { - result.push({amount: t.amount, date: getDateString(t.dateTime)}) + result.push({amount: t.amount, date: getDateString(t.dateTime), goal: 0}) }) + // Change the last bit of data to set the goal + const lastElem = result[result.length - 1] + result[result.length - 1] = {amount: lastElem.amount, date: lastElem.date, goal: goal} + return splitTransactions(result) } export const splitTransactions = (data: transactionPoint[]): transactionPoint[][] => { diff --git a/src/pages/dashboard/Graphs.tsx b/src/pages/dashboard/Graphs.tsx index 8bf93e7..50682d9 100644 --- a/src/pages/dashboard/Graphs.tsx +++ b/src/pages/dashboard/Graphs.tsx @@ -1,6 +1,6 @@ -import {Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis} from "recharts"; +import {Line, LineChart, ResponsiveContainer, XAxis, YAxis} from "recharts"; -type transactionPoint = { date: string; amount: number } +type transactionPoint = { date: string; amount: number; goal: number } interface Props { data: transactionPoint[]; @@ -12,8 +12,8 @@ export default function Graphs({data}: Props) { - + ); From 5c2834e0710abc04909db46d1b80a1686e259d5e Mon Sep 17 00:00:00 2001 From: Robert Lucas <100799838+Robert-M-Lucas@users.noreply.github.com> Date: Sat, 27 Apr 2024 21:13:22 +0100 Subject: [PATCH 08/10] Added basic goal tile --- src/pages/dashboard/DashboardPage.tsx | 23 +++++++------------ src/pages/dashboard/GoalsTile.tsx | 7 ++++++ .../dashboard/{Tiles.tsx => TotalTile.tsx} | 0 3 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 src/pages/dashboard/GoalsTile.tsx rename src/pages/dashboard/{Tiles.tsx => TotalTile.tsx} (100%) diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx index 7be0ea7..7004cbf 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/pages/dashboard/DashboardPage.tsx @@ -7,19 +7,22 @@ import {useEffect, useState} from "react"; import {Transaction, getTransactionsFilterOrderBy } from "../../utils/transaction.ts" import {auth} from "../../utils/firebase.ts"; import {orderBy} from "firebase/firestore"; -import { User } from "firebase/auth"; import {FullscreenCenter} from "../../components/FullscreenCenter.tsx"; import Graphs from "./Graphs.tsx" import {getTileSize, TileElement} from "./TileUtils.ts"; import {finalGraphData, readTransactions} from "./GraphUtils.ts"; import {signInWithGoogle} from "../../utils/authentication.ts"; -import totalTile from "./Tiles.tsx"; +import totalTile from "./TotalTile.tsx"; +import {getUserPrefs, UserPrefs} from "../../utils/user_prefs.ts"; +import {User} from "firebase/auth"; +import goalsTile from "./GoalsTile.tsx"; export default function Dashboard() { // const [balance, setBalance] = useState(0); const [transactionPoints, setPoints] = useState(null); const [transactions, setTransactions] = useState([]); const [authResolved, setAuthResolved] = useState(false); + const [userPrefs, setUserPrefs] = useState(null); // const [showCSVModal, setShowCSVModal] = useState(false); // const [showTransactionModal, setShowTransactionModal] = useState(false); const [update, setUpdate] = useState(0) @@ -37,6 +40,7 @@ export default function Dashboard() { useEffect(() => { if (auth.currentUser !== null) { fetchTransactions(auth.currentUser).then(); + getUserPrefs(auth.currentUser).then((prefs) => setUserPrefs(prefs)); } },[auth.currentUser]) @@ -69,7 +73,7 @@ export default function Dashboard() { ; } - if (!transactionPoints) { + if (!transactionPoints || !userPrefs) { return <>
    @@ -82,6 +86,7 @@ export default function Dashboard() { const transactionTiles: TileElement[] = [ TileElement.newTSX(() => totalTile(transactions), 2, 1, columns), + TileElement.newTSX(() => (goalsTile(userPrefs)), 2, 1, columns), TileElement.newGraph(transactionPoints.raw, 3, 2, columns), TileElement.newGraph(transactionPoints.in, 3, 2, columns), TileElement.newGraph(transactionPoints.out, 3, 2, columns), @@ -99,19 +104,7 @@ export default function Dashboard() { return (
    - {/*
    */} - {/* */} - {/* */} - - {/* */} - {/* */} - {/*
    */}
    - {/**/} - {/*{balance}*/} Goal: {userPrefs.getNeedsBudget()} | {userPrefs.getWantsBudget()} | {userPrefs.getSavingsBudget()}

    ; +} \ No newline at end of file diff --git a/src/pages/dashboard/Tiles.tsx b/src/pages/dashboard/TotalTile.tsx similarity index 100% rename from src/pages/dashboard/Tiles.tsx rename to src/pages/dashboard/TotalTile.tsx From 2b939612b6e54a83ec9e627f1303df822dddcfd7 Mon Sep 17 00:00:00 2001 From: Robert Lucas <100799838+Robert-M-Lucas@users.noreply.github.com> Date: Sat, 27 Apr 2024 21:25:05 +0100 Subject: [PATCH 09/10] Merge --- src/pages/dashboard/DashboardPage.tsx | 4 ++-- src/pages/dashboard/GraphUtils.ts | 4 ++-- src/pages/dashboard/Graphs.tsx | 3 --- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx index 7004cbf..103dad5 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/pages/dashboard/DashboardPage.tsx @@ -50,7 +50,7 @@ export default function Dashboard() {
    -

    Waiting for Auth

    +

    Waiting for Authentication

    ; @@ -78,7 +78,7 @@ export default function Dashboard() {
    -

    Fetching

    +

    Fetching {transactionPoints ? "" : "transactions"}{!transactionPoints && !userPrefs ? "," : ""} {userPrefs ? "" : "goals"}

    ; diff --git a/src/pages/dashboard/GraphUtils.ts b/src/pages/dashboard/GraphUtils.ts index cad8f00..09edd4b 100644 --- a/src/pages/dashboard/GraphUtils.ts +++ b/src/pages/dashboard/GraphUtils.ts @@ -1,7 +1,7 @@ import {Transaction} from "../../utils/transaction.ts"; import strftime from "strftime"; -export type transactionPoint = { date: string; amount: number } +type transactionPoint = { date: string; amount: number; goal: number } export type finalGraphData = {raw: transactionPoint[], in: transactionPoint[], out: transactionPoint[]}; function cumulateTransactions(points: transactionPoint[]): transactionPoint[] { @@ -33,7 +33,7 @@ function splitTransactions (data: transactionPoint[]): finalGraphData { export function readTransactions(data: Transaction[]): finalGraphData { return splitTransactions( data.map((t) => { - return {date: getDateString(t.dateTime), amount: t.amount} + return {date: getDateString(t.dateTime), amount: t.amount, goal: 800}; }) ); } \ No newline at end of file diff --git a/src/pages/dashboard/Graphs.tsx b/src/pages/dashboard/Graphs.tsx index ebd2a4f..098d6c8 100644 --- a/src/pages/dashboard/Graphs.tsx +++ b/src/pages/dashboard/Graphs.tsx @@ -1,6 +1,3 @@ -import {Line, LineChart, ResponsiveContainer, XAxis, YAxis} from "recharts"; - -type transactionPoint = { date: string; amount: number; goal: number } import {Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis} from "recharts"; import {transactionPoint} from "./GraphUtils.ts"; From 423f6a9f7e984a1ce522b57153557d33a2f0ec97 Mon Sep 17 00:00:00 2001 From: Robert Lucas <100799838+Robert-M-Lucas@users.noreply.github.com> Date: Sat, 27 Apr 2024 23:24:48 +0100 Subject: [PATCH 10/10] Finished Goals tile --- package-lock.json | 6 + package.json | 1 + src/pages/dashboard/DashboardPage.tsx | 24 +- src/pages/dashboard/GoalsTile.tsx | 7 - src/pages/dashboard/TileUtils.ts | 2 +- src/pages/dashboard/goals tile/GoalsTile.css | 227 ++++++++++++++++++ .../dashboard/goals tile/GoalsTile.css.map | 1 + src/pages/dashboard/goals tile/GoalsTile.scss | 199 +++++++++++++++ src/pages/dashboard/goals tile/GoalsTile.tsx | 107 +++++++++ .../dashboard/{ => graphs}/GraphUtils.ts | 4 +- src/pages/dashboard/{ => graphs}/Graphs.tsx | 0 .../dashboard/{ => total tile}/TotalTile.tsx | 2 +- src/utils/user_prefs.ts | 6 +- 13 files changed, 563 insertions(+), 23 deletions(-) delete mode 100644 src/pages/dashboard/GoalsTile.tsx create mode 100644 src/pages/dashboard/goals tile/GoalsTile.css create mode 100644 src/pages/dashboard/goals tile/GoalsTile.css.map create mode 100644 src/pages/dashboard/goals tile/GoalsTile.scss create mode 100644 src/pages/dashboard/goals tile/GoalsTile.tsx rename src/pages/dashboard/{ => graphs}/GraphUtils.ts (89%) rename src/pages/dashboard/{ => graphs}/Graphs.tsx (100%) rename src/pages/dashboard/{ => total tile}/TotalTile.tsx (97%) diff --git a/package-lock.json b/package-lock.json index c65656a..270b0cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "bootstrap": "^5.3.3", "firebase": "^10.11.0", "lodash": "^4.17.21", + "multi-range-slider-react": "^2.0.7", "papaparse": "^5.4.1", "react": "^18.2.0", "react-bootstrap": "^2.10.2", @@ -5544,6 +5545,11 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/multi-range-slider-react": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/multi-range-slider-react/-/multi-range-slider-react-2.0.7.tgz", + "integrity": "sha512-KRYUkatXxxYceL5ZT8xvetIN+4yTCdWszxRC6Y6Jkua+oRrWVkmBR6v3R03kosYg/QtcETBf2L1Jt+4U66DFbg==" + }, "node_modules/nano-css": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.1.tgz", diff --git a/package.json b/package.json index 4dce34f..548026a 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "bootstrap": "^5.3.3", "firebase": "^10.11.0", "lodash": "^4.17.21", + "multi-range-slider-react": "^2.0.7", "papaparse": "^5.4.1", "react": "^18.2.0", "react-bootstrap": "^2.10.2", diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx index 103dad5..dce11e9 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/pages/dashboard/DashboardPage.tsx @@ -1,6 +1,5 @@ import "./styles.css"; 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 {useEffect, useState} from "react"; @@ -8,25 +7,32 @@ import {Transaction, getTransactionsFilterOrderBy } from "../../utils/transactio import {auth} from "../../utils/firebase.ts"; import {orderBy} from "firebase/firestore"; import {FullscreenCenter} from "../../components/FullscreenCenter.tsx"; -import Graphs from "./Graphs.tsx" +import Graphs from "./graphs/Graphs.tsx" import {getTileSize, TileElement} from "./TileUtils.ts"; -import {finalGraphData, readTransactions} from "./GraphUtils.ts"; +import {finalGraphData, readTransactions} from "./graphs/GraphUtils.ts"; import {signInWithGoogle} from "../../utils/authentication.ts"; -import totalTile from "./TotalTile.tsx"; +import totalTile from "./total tile/TotalTile.tsx"; import {getUserPrefs, UserPrefs} from "../../utils/user_prefs.ts"; import {User} from "firebase/auth"; -import goalsTile from "./GoalsTile.tsx"; +import goalsTile from "./goals tile/GoalsTile.tsx"; +import {RenderTileFunction, TilesContainer} from "react-tiles-dnd"; export default function Dashboard() { // const [balance, setBalance] = useState(0); const [transactionPoints, setPoints] = useState(null); const [transactions, setTransactions] = useState([]); - const [authResolved, setAuthResolved] = useState(false); + const [authResolved, setAuthResolved] = useState(false); const [userPrefs, setUserPrefs] = useState(null); + // const draggable = useRef(true); // const [showCSVModal, setShowCSVModal] = useState(false); // const [showTransactionModal, setShowTransactionModal] = useState(false); const [update, setUpdate] = useState(0) + const forceUpdate = () => { + setUpdate(update + 1); + setUserPrefs(null); + }; + const fetchTransactions = async (user: User) => { const transactions = await getTransactionsFilterOrderBy(user, orderBy("dateTime", "desc")); setTransactions(transactions); @@ -42,7 +48,7 @@ export default function Dashboard() { fetchTransactions(auth.currentUser).then(); getUserPrefs(auth.currentUser).then((prefs) => setUserPrefs(prefs)); } - },[auth.currentUser]) + },[auth.currentUser, update]); if (!authResolved) { auth.authStateReady().then(() => setAuthResolved(true)); @@ -86,13 +92,13 @@ export default function Dashboard() { const transactionTiles: TileElement[] = [ TileElement.newTSX(() => totalTile(transactions), 2, 1, columns), - TileElement.newTSX(() => (goalsTile(userPrefs)), 2, 1, columns), + TileElement.newTSX(() => (goalsTile(userPrefs, forceUpdate)), 2, 2, columns), TileElement.newGraph(transactionPoints.raw, 3, 2, columns), TileElement.newGraph(transactionPoints.in, 3, 2, columns), TileElement.newGraph(transactionPoints.out, 3, 2, columns), ]; - const renderTile: RenderTileFunction = ({ data, isDragging }) => ( + const renderTile: RenderTileFunction = ({ data, isDragging }) => (
    diff --git a/src/pages/dashboard/GoalsTile.tsx b/src/pages/dashboard/GoalsTile.tsx deleted file mode 100644 index 8232909..0000000 --- a/src/pages/dashboard/GoalsTile.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import {ReactNode} from "react"; -import {UserPrefs} from "../../utils/user_prefs.ts"; - -export default function goalsTile(userPrefs: UserPrefs): ReactNode { - - return

    Goal: {userPrefs.getNeedsBudget()} | {userPrefs.getWantsBudget()} | {userPrefs.getSavingsBudget()}

    ; -} \ No newline at end of file diff --git a/src/pages/dashboard/TileUtils.ts b/src/pages/dashboard/TileUtils.ts index 3fbee2b..a18e05f 100644 --- a/src/pages/dashboard/TileUtils.ts +++ b/src/pages/dashboard/TileUtils.ts @@ -1,5 +1,5 @@ import {ReactNode} from "react"; -import {transactionPoint} from "./GraphUtils.ts"; +import {transactionPoint} from "./graphs/GraphUtils.ts"; import {min} from "lodash"; type tsxContents = ReactNode; diff --git a/src/pages/dashboard/goals tile/GoalsTile.css b/src/pages/dashboard/goals tile/GoalsTile.css new file mode 100644 index 0000000..27ca49e --- /dev/null +++ b/src/pages/dashboard/goals tile/GoalsTile.css @@ -0,0 +1,227 @@ +.modified-multi-range-slider * { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +.modified-multi-range-slider { + display: flex; + position: relative; + padding: 20px 10px; + flex-direction: column; + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Old versions of Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge,*/ +} + +.modified-multi-range-slider .bar { + display: flex; +} + +.modified-multi-range-slider .bar-left { + width: 25%; + background-color: rgb(202, 15, 15); + border-radius: 10px 0 0 10px; + padding: 4px 0; +} + +.modified-multi-range-slider .bar-right { + width: 25%; + background-color: green; + border-radius: 0 10px 10px 0; +} + +.modified-multi-range-slider .bar-inner { + background-color: #0000bf; + display: flex; + flex-grow: 1; + flex-shrink: 1; + justify-content: space-between; + position: relative; +} + +.modified-multi-range-slider .bar-inner-left { + width: 50%; +} + +.modified-multi-range-slider .bar-inner-right { + width: 50%; +} + +.modified-multi-range-slider .thumb { + background-color: #d6cfcf; + position: relative; + z-index: 1; + cursor: pointer; +} + +.modified-multi-range-slider .thumb::before { + content: ""; + background-color: inherit; + position: absolute; + top: 3px; + width: 17px; + height: 17px; + border: solid 1px #979797; + border-radius: 50%; + z-index: 1; + margin: -8px -12px; + cursor: pointer; +} + +.modified-multi-range-slider .input-type-range:focus + .thumb::after { + content: ""; + position: absolute; + top: -4px; + left: -7px; + width: 11px; + height: 11px; + z-index: 2; + border-radius: 50%; + border: dotted 1px black; + box-shadow: 0 0 5px white, inset 0 0 10px black; +} + +.modified-multi-range-slider .caption { + display: none; +} + +.modified-multi-range-slider .thumb .caption * { + display: none; +} + +.modified-multi-range-slider .thumb:active .caption { + display: none; +} + +.modified-multi-range-slider .input-type-range:focus + .thumb .caption { + display: flex; +} + +.modified-multi-range-slider .input-type-range { + position: absolute; + top: 0; + left: 0; + width: 100%; + opacity: 0; + pointer-events: none; +} + +.modified-multi-range-slider .ruler { + margin: 10px 0 -5px 0; + display: flex; + /* display: none; */ + overflow: hidden; +} + +.modified-multi-range-slider .ruler .ruler-rule { + border-left: solid 1px; + border-bottom: solid 1px; + display: flex; + flex-grow: 1; + flex-shrink: 1; + padding: 5px 0; +} + +.modified-multi-range-slider .ruler .ruler-rule:last-child { + border-right: solid 1px; +} + +.modified-multi-range-slider .ruler .ruler-sub-rule { + border-left: solid 1px; + /* border-bottom: solid 1px; */ + display: flex; + flex-grow: 1; + flex-shrink: 1; + padding: 3px 0; + bottom: 0; + margin-bottom: -5px; +} + +.modified-multi-range-slider .ruler .ruler-sub-rule:first-child { + border-left: none; +} + +.modified-multi-range-slider .labels { + display: flex; + justify-content: space-between; + padding: 0; + margin-top: 10px; + margin-bottom: -20px; + /* display: none; */ +} + +.modified-multi-range-slider .label { + font-size: 80%; + display: flex; + width: 1px; + justify-content: center; +} + +.modified-multi-range-slider .label:first-child { + justify-content: start; +} + +.modified-multi-range-slider .label:last-child { + justify-content: end; +} + +.modified-multi-range-slider.zero-ranage-margin .thumb-left { + right: 12px; +} + +.modified-multi-range-slider.zero-ranage-margin .thumb-right { + left: 8px; +} + +/* Disabled */ +.modified-multi-range-slider.disabled { + border: solid 1px rgb(200, 200, 200); + box-shadow: 1px 1px 4px rgb(180, 180, 180); + color: rgb(180, 180, 180); +} + +.modified-multi-range-slider .bar { + display: flex; +} + +.modified-multi-range-slider.disabled .bar-left { + background-color: #c9c9c9; + box-shadow: inset 0 0 5px rgb(160, 160, 160); +} + +.modified-multi-range-slider.disabled .bar-right { + background-color: #c9c9c9; + box-shadow: inset 0 0 5px rgb(160, 160, 160); +} + +.modified-multi-range-slider.disabled .bar-inner { + background-color: rgb(130, 243, 130); + border: solid 1px rgb(149, 149, 149); + box-shadow: inset 0 0 5px rgb(103, 103, 103); +} + +.modified-multi-range-slider.disabled .thumb { + background-color: white; +} + +.modified-multi-range-slider.disabled .thumb::before { + border: solid 1px rgb(200, 200, 200); + box-shadow: 0 0 3px rgb(35, 35, 35), inset 0 0 5px gray; +} + +.modified-multi-range-slider.disabled .input-type-range:focus + .thumb::after { + border: dotted 1px rgb(35, 35, 35); + box-shadow: 0 0 5px white, inset 0 0 10px rgb(35, 35, 35); +} + +.modified-multi-range-slider.disabled .thumb .caption * { + background-color: rgb(84, 84, 137); + color: rgb(199, 199, 199); + box-shadow: 0 0 5px rgb(35, 35, 35); +} + +/*# sourceMappingURL=GoalsTile.css.map */ diff --git a/src/pages/dashboard/goals tile/GoalsTile.css.map b/src/pages/dashboard/goals tile/GoalsTile.css.map new file mode 100644 index 0000000..203b8eb --- /dev/null +++ b/src/pages/dashboard/goals tile/GoalsTile.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["GoalsTile.scss"],"names":[],"mappings":"AAAA;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;AACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAGF;EACE;AACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;AACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAIF;AACA;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA","file":"GoalsTile.css"} \ No newline at end of file diff --git a/src/pages/dashboard/goals tile/GoalsTile.scss b/src/pages/dashboard/goals tile/GoalsTile.scss new file mode 100644 index 0000000..b05c5a5 --- /dev/null +++ b/src/pages/dashboard/goals tile/GoalsTile.scss @@ -0,0 +1,199 @@ +.modified-multi-range-slider * { + box-sizing: border-box; + padding: 0; + margin: 0; +} +.modified-multi-range-slider { + display: flex; + position: relative; + padding: 20px 10px; + flex-direction: column; + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Old versions of Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge,*/ +} +.modified-multi-range-slider .bar { + display: flex; +} +.modified-multi-range-slider .bar-left { + width: 25%; + background-color: rgb(202, 15, 15); + border-radius: 10px 0 0 10px; + padding: 4px 0; +} +.modified-multi-range-slider .bar-right { + width: 25%; + background-color: green; + border-radius: 0 10px 10px 0; +} +.modified-multi-range-slider .bar-inner { + background-color: #0000bf; + display: flex; + flex-grow: 1; + flex-shrink: 1; + justify-content: space-between; + position: relative; +} +.modified-multi-range-slider .bar-inner-left { + width: 50%; +} +.modified-multi-range-slider .bar-inner-right { + width: 50%; +} +.modified-multi-range-slider .thumb { + background-color: #d6cfcf; + position: relative; + z-index: 1; + cursor: pointer; +} + +.modified-multi-range-slider .thumb::before { + content: ''; + background-color: inherit; + position: absolute; + top: 3px; + width: 17px; + height: 17px; + border: solid 1px #979797; + border-radius: 50%; + z-index: 1; + margin: -8px -12px; + cursor: pointer; +} +.modified-multi-range-slider .input-type-range:focus + .thumb::after { + content: ''; + position: absolute; + top: -4px; + left: -7px; + width: 11px; + height: 11px; + z-index: 2; + border-radius: 50%; + border: dotted 1px black; + box-shadow: 0 0 5px white, inset 0 0 10px black; +} +.modified-multi-range-slider .caption { + display: none; +} +.modified-multi-range-slider .thumb .caption * { + display: none; +} +.modified-multi-range-slider .thumb:active .caption { + display: none; +} +.modified-multi-range-slider .input-type-range:focus + .thumb .caption { + display: flex; +} + +.modified-multi-range-slider .input-type-range { + position: absolute; + top: 0; + left: 0; + width: 100%; + opacity: 0; + pointer-events: none; +} + +.modified-multi-range-slider .ruler { + margin: 10px 0 -5px 0; + display: flex; + /* display: none; */ + overflow: hidden; +} +.modified-multi-range-slider .ruler .ruler-rule { + border-left: solid 1px; + border-bottom: solid 1px; + display: flex; + flex-grow: 1; + flex-shrink: 1; + padding: 5px 0; +} +.modified-multi-range-slider .ruler .ruler-rule:last-child { + border-right: solid 1px; +} + +.modified-multi-range-slider .ruler .ruler-sub-rule { + border-left: solid 1px; + /* border-bottom: solid 1px; */ + display: flex; + flex-grow: 1; + flex-shrink: 1; + padding: 3px 0; + bottom: 0; + margin-bottom: -5px; +} +.modified-multi-range-slider .ruler .ruler-sub-rule:first-child { + border-left: none; +} + +.modified-multi-range-slider .labels { + display: flex; + justify-content: space-between; + padding: 0; + margin-top: 10px; + margin-bottom: -20px; + /* display: none; */ +} +.modified-multi-range-slider .label { + font-size: 80%; + display: flex; + width: 1px; + justify-content: center; +} +.modified-multi-range-slider .label:first-child { + justify-content: start; +} +.modified-multi-range-slider .label:last-child { + justify-content: end; +} +.modified-multi-range-slider.zero-ranage-margin .thumb-left { + right: 12px; +} +.modified-multi-range-slider.zero-ranage-margin .thumb-right { + left: 8px; +} + + +/* Disabled */ +.modified-multi-range-slider.disabled { + border: solid 1px rgb(200, 200, 200); + box-shadow: 1px 1px 4px rgb(180, 180, 180); + color:rgb(180, 180, 180); +} +.modified-multi-range-slider .bar { + display: flex; +} +.modified-multi-range-slider.disabled .bar-left { + background-color: #c9c9c9; + box-shadow: inset 0 0 5px rgb(160, 160, 160); +} +.modified-multi-range-slider.disabled .bar-right { + background-color: #c9c9c9; + box-shadow: inset 0 0 5px rgb(160, 160, 160); +} +.modified-multi-range-slider.disabled .bar-inner { + background-color: rgb(130 243 130); + border: solid 1px rgb(149, 149, 149); + box-shadow: inset 0 0 5px rgb(103, 103, 103); +} +.modified-multi-range-slider.disabled .thumb { + background-color: white; +} +.modified-multi-range-slider.disabled .thumb::before { + border: solid 1px rgb(200, 200, 200); + box-shadow: 0 0 3px rgb(35, 35, 35), inset 0 0 5px gray; +} +.modified-multi-range-slider.disabled .input-type-range:focus + .thumb::after { + border: dotted 1px rgb(35, 35, 35); + box-shadow: 0 0 5px white, inset 0 0 10px rgb(35, 35, 35); +} +.modified-multi-range-slider.disabled .thumb .caption * { + background-color: rgb(84, 84, 137); + color: rgb(199, 199, 199); + box-shadow: 0 0 5px rgb(35, 35, 35); +} + + diff --git a/src/pages/dashboard/goals tile/GoalsTile.tsx b/src/pages/dashboard/goals tile/GoalsTile.tsx new file mode 100644 index 0000000..313b5b9 --- /dev/null +++ b/src/pages/dashboard/goals tile/GoalsTile.tsx @@ -0,0 +1,107 @@ +import React, {ReactNode, useState} from "react"; +import {setUserPrefs, UserPrefs} from "../../../utils/user_prefs.ts"; +import MultiRangeSlider, {ChangeResult} from "multi-range-slider-react"; +import "./GoalsTile.scss"; +import {auth} from "../../../utils/firebase.ts"; + +export default function goalsTile(userPrefs: UserPrefs, forceUpdate: () => void): ReactNode { + const [minValue, setMinValue] = useState( + Math.round(userPrefs.getNeedsBudget() * 100) + ); + const [maxValue, setMaxValue] = useState( + Math.round((userPrefs.getNeedsBudget() + userPrefs.getWantsBudget()) * 100) + ); + + const needs = minValue; + const wants = maxValue - minValue; + const savings = 100 - maxValue; + + const disableSet = needs === Math.round(userPrefs.getNeedsBudget() * 100) && wants === Math.round(userPrefs.getWantsBudget() * 100); + const disableRecommended = needs === 50 && wants === 30; + + const handleInput = (e: ChangeResult) => { + const min = e.minValue; + const max = e.maxValue; + + setMinValue(min); + setMaxValue(max); + }; + + const stop = (e: React.PointerEvent) => { + e.stopPropagation(); + e.nativeEvent.stopImmediatePropagation(); + } + + return <> +
    + Goals +
    +
      +
    • +
      +
      Needs:
      +
      {needs}% +
      +
      +
    • +
    • +
      +
      Wants:
      +
      {wants}% +
      +
      +
    • +
    • +
      +
      Savings:
      +
      {savings}% +
      +
      +
    • +
    • +
      {/* THIS LINE TOOK 1.5H */} + +
      +
    • +
    +
    +
    + + +
    +
    + ; +} \ No newline at end of file diff --git a/src/pages/dashboard/GraphUtils.ts b/src/pages/dashboard/graphs/GraphUtils.ts similarity index 89% rename from src/pages/dashboard/GraphUtils.ts rename to src/pages/dashboard/graphs/GraphUtils.ts index 09edd4b..dab63e1 100644 --- a/src/pages/dashboard/GraphUtils.ts +++ b/src/pages/dashboard/graphs/GraphUtils.ts @@ -1,7 +1,7 @@ -import {Transaction} from "../../utils/transaction.ts"; +import {Transaction} from "../../../utils/transaction.ts"; import strftime from "strftime"; -type transactionPoint = { date: string; amount: number; goal: number } +export type transactionPoint = { date: string; amount: number; goal: number } export type finalGraphData = {raw: transactionPoint[], in: transactionPoint[], out: transactionPoint[]}; function cumulateTransactions(points: transactionPoint[]): transactionPoint[] { diff --git a/src/pages/dashboard/Graphs.tsx b/src/pages/dashboard/graphs/Graphs.tsx similarity index 100% rename from src/pages/dashboard/Graphs.tsx rename to src/pages/dashboard/graphs/Graphs.tsx diff --git a/src/pages/dashboard/TotalTile.tsx b/src/pages/dashboard/total tile/TotalTile.tsx similarity index 97% rename from src/pages/dashboard/TotalTile.tsx rename to src/pages/dashboard/total tile/TotalTile.tsx index 6bad195..3507508 100644 --- a/src/pages/dashboard/TotalTile.tsx +++ b/src/pages/dashboard/total tile/TotalTile.tsx @@ -1,5 +1,5 @@ import {ReactNode} from "react"; -import { Transaction } from "../../utils/transaction"; +import { Transaction } from "../../../utils/transaction.ts"; import {max, min} from "lodash"; export default function totalTile(transactions: Transaction[]): ReactNode { diff --git a/src/utils/user_prefs.ts b/src/utils/user_prefs.ts index 24ebba6..fc822c4 100644 --- a/src/utils/user_prefs.ts +++ b/src/utils/user_prefs.ts @@ -21,13 +21,13 @@ export class UserPrefs { this.wantsBudget = round(wantsBudget, 2); } - static newChecked(needsBudget: number, wantsBudget: number): UserPrefs | Error { + static newChecked(needsBudget: number, wantsBudget: number): UserPrefs { if (needsBudget > 1) { - return new Error("needsBudget > 1!"); + throw new Error("needsBudget > 1!"); } if (needsBudget + wantsBudget > 1) { - return new Error("needsBudget + wantsBudget > 1!"); + throw new Error("needsBudget + wantsBudget > 1!"); } return new UserPrefs(needsBudget, wantsBudget);