diff --git a/assets/images/logo.png b/assets/images/logo.png new file mode 100644 index 0000000..e270d85 Binary files /dev/null and b/assets/images/logo.png differ diff --git a/components/CategoryChart.tsx b/components/CategoryChart.tsx new file mode 100644 index 0000000..d2d37e8 --- /dev/null +++ b/components/CategoryChart.tsx @@ -0,0 +1,127 @@ +import React from "react"; +import { useIntl } from "react-intl"; +import { useQuery, gql } from "@apollo/client"; +import { PieChart, Pie, LabelList } from "recharts"; +import Dinero from "dinero.js"; +import Loading from "components/Loading"; + +const GET_CATEGORY_CHART_DATA = gql` + query categoryChartData { + queryTransaction { + id + amount + category { + id + icon + name + } + } + } +`; + +const CategoryChart: React.FC = () => { + const intl = useIntl(); + const { + data: pieChartData, + loading, + error, + } = useQuery(GET_CATEGORY_CHART_DATA, {}); + if (error) { + console.error(error); + } + + const formatChartData = () => { + let data: { + name: string; + icon: string; + value: number; + categoryId: string; + }[] = []; + + if (!pieChartData || !pieChartData.queryTransaction) { + return data; + } + + pieChartData.queryTransaction.forEach((item: any) => { + const categoryIndex = data.findIndex( + (c) => c.categoryId === item.category.id + ); + if (item.amount < 0) { + if (categoryIndex !== -1) { + data[categoryIndex].value += Math.abs(item.amount); + } else { + data.push({ + icon: item.category.icon, + name: item.category.name, + value: Math.abs(item.amount), + categoryId: item.category.id, + }); + } + } + }); + + return data; + }; + + if (loading) { + return ; + } + + return ( +
+ + + { + // console.log("value", value); + // return value; + // }} + /> + + +
+

+ {intl.formatMessage({ defaultMessage: "Spend by category" })} +

+ {formatChartData().map((item: any) => ( +
+
+
+ {item.icon} +
+
+

{item.name}

+
+
+

+ -  + {Dinero({ amount: item.value, precision: 2 }).toFormat("$0,0.00")} +

+
+ ))} +
+
+ ); +}; + +export default CategoryChart; diff --git a/components/HeaderNavbar.tsx b/components/HeaderNavbar.tsx new file mode 100644 index 0000000..cd6ebf6 --- /dev/null +++ b/components/HeaderNavbar.tsx @@ -0,0 +1,99 @@ +import React, { useState } from "react"; +import Image from "next/image"; +import { useIntl } from "react-intl"; +import { useSession } from "next-auth/react"; +import toast from "react-hot-toast"; +import { gql, useMutation } from "@apollo/client"; +import { parseCurrency } from "utils/currency"; +import { Dialog } from "@headlessui/react"; +import TransactionForm, { TransactionFormValues } from "forms/TransactionForm"; +import { + GET_TRANSACTIONS, + TRANSACTION_AMOUNT_AGGREGATE, +} from "constants/queries"; +import logo from "assets/images/logo.png"; + +const ADD_TRANSACTION = gql` + mutation AddTransaction($input: [AddTransactionInput!]!) { + addTransaction(input: $input) { + transaction { + id + } + } + } +`; + +const HeaderNavbar: React.FC = () => { + const intl = useIntl(); + const { data: session } = useSession(); + const [addTransaction] = useMutation(ADD_TRANSACTION, {}); + + const [open, setOpen] = useState(false); + + const onNewTransaction = async (values: TransactionFormValues) => { + try { + let amount = values.type === "income" ? values.amount : -values.amount; + + await addTransaction({ + variables: { + input: [ + { + amount: parseCurrency(amount), + date: values.date, + category: { id: values.category }, + user: { + // @ts-ignore + id: session.user?.id, + }, + }, + ], + }, + refetchQueries: [GET_TRANSACTIONS, TRANSACTION_AMOUNT_AGGREGATE], + }); + toast.success( + intl.formatMessage({ defaultMessage: "Transaction added" }) + ); + setOpen(false); + } catch (err: any) { + console.error(err); + toast.error(err.message); + } + }; + + return ( +
+
+
+ + logo + profile +
+
+ setOpen(false)}> + +
+
+ +
+
+
+
+ ); +}; + +export default HeaderNavbar; diff --git a/components/MobileNavbar.tsx b/components/MobileNavbar.tsx index 53a9188..d61b055 100644 --- a/components/MobileNavbar.tsx +++ b/components/MobileNavbar.tsx @@ -87,7 +87,7 @@ const MobileNavbar: React.FC = () => { return ( -
+
setOpen(false)}> -
+
diff --git a/components/Protected.tsx b/components/Protected.tsx index 8b3e7a3..a73cc9b 100644 --- a/components/Protected.tsx +++ b/components/Protected.tsx @@ -21,6 +21,7 @@ import { useIntl } from "react-intl"; import toast from "react-hot-toast"; import { UserContext } from "contexts/User"; import Loading from "components/Loading"; +import HeaderNavbar from "components/HeaderNavbar"; import MobileNavbar from "components/MobileNavbar"; const Protected: React.FC = (props) => { @@ -46,6 +47,7 @@ const Protected: React.FC = (props) => { return (
+ {session?.user && }
{props.children}
{session?.user && }
diff --git a/next.config.js b/next.config.js index 08a8de5..e4a2f0b 100644 --- a/next.config.js +++ b/next.config.js @@ -26,7 +26,7 @@ const moduleExports = { localeDetection: true, }, images: { - domains: [], + domains: ["lh3.googleusercontent.com"], }, webpack(config, { dev, ...other }) { if (!dev) { diff --git a/pages/account/categories.tsx b/pages/account/categories.tsx index b8af991..6de2595 100644 --- a/pages/account/categories.tsx +++ b/pages/account/categories.tsx @@ -112,7 +112,10 @@ const Categories: NextPage = () => {
setOpen(false)}> -
+
diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx new file mode 100644 index 0000000..0387253 --- /dev/null +++ b/pages/dashboard.tsx @@ -0,0 +1,95 @@ +import type { NextPage } from "next"; +import { Fragment } from "react"; +import { useIntl } from "react-intl"; +import { useQuery, gql } from "@apollo/client"; +import toast from "react-hot-toast"; +import { TRANSACTION_AMOUNT_AGGREGATE } from "constants/queries"; +import { GET_TRANSACTIONS } from "constants/queries"; +import Protected from "components/Protected"; +import LargeNumberCard from "components/LargeNumberCard"; +import SmallNumberCard from "components/SmallNumberCard"; +import Transaction from "components/Transaction"; +import CategoryChart from "components/CategoryChart"; + +const Dashboard: NextPage = () => { + const intl = useIntl(); + const { data: income, error: incomeError } = useQuery( + TRANSACTION_AMOUNT_AGGREGATE, + { + variables: { filter: { and: [{ amount: { gt: 0 } }] } }, + } + ); + if (incomeError) { + console.error(incomeError); + toast.error(incomeError.message); + } + const { data: expense, error: expenseError } = useQuery( + TRANSACTION_AMOUNT_AGGREGATE, + { + variables: { filter: { and: [{ amount: { lt: 0 } }] } }, + } + ); + if (expenseError) { + console.error(expenseError); + toast.error(expenseError.message); + } + + const { data: transactions, error } = useQuery(GET_TRANSACTIONS, { + variables: { + order: { desc: "date" }, + limit: 30, + }, + }); + if (error) { + console.error(error); + toast.error(error.message); + } + + return ( + +
+
+
+
+ +
+ + +
+
+
+ +
+
+
+ {!transactions?.queryTransaction.length ? ( +

+ {intl.formatMessage({ defaultMessage: "No transactions" })} +

+ ) : ( + + {transactions.queryTransaction.map((transaction: any) => ( + + ))} + + )} +
+
+
+
+ ); +}; + +export default Dashboard; diff --git a/pages/index.tsx b/pages/index.tsx index b247ad9..e601354 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -15,6 +15,7 @@ */ import type { NextPage } from "next"; +import { useEffect } from "react"; import { useIntl } from "react-intl"; import { useQuery } from "@apollo/client"; import toast from "react-hot-toast"; @@ -47,9 +48,16 @@ const Home: NextPage = () => { toast.error(expenseError.message); } + useEffect(() => { + const width = screen.width; + if (width > 992) { + window.location.replace("/dashboard"); + } + }, []); + return ( -
+