├── public ├── _redirects ├── robots.txt ├── favicon.ico ├── img │ ├── Arch.png │ ├── screens │ │ ├── Arch.png │ │ ├── home.png │ │ ├── cover.png │ │ ├── splash.png │ │ ├── types.png │ │ ├── analytics.png │ │ ├── onboarding.png │ │ ├── home_success.png │ │ └── types_details.png │ └── spreadsheetSS.png ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── src ├── context │ ├── types │ │ ├── Global.js │ │ ├── Modal.js │ │ ├── Search.js │ │ ├── index.js │ │ └── User.js │ ├── reducers │ │ ├── Search.js │ │ ├── Modal.js │ │ └── User.js │ └── index.js ├── services │ ├── index.js │ ├── auth.js │ ├── configSpreadsheet.js │ └── headerData.js ├── rsc │ ├── img │ │ ├── emptybox.png │ │ ├── iosInstall.png │ │ ├── androidInstall.png │ │ ├── loginScreenshot.png │ │ ├── spreadsheetScreenshot.png │ │ ├── bg.svg │ │ └── logo.svg │ └── icons │ │ └── github.svg ├── lib │ └── utils │ │ ├── colors.js │ │ ├── toast.js │ │ ├── index.js │ │ ├── common.js │ │ ├── currency.js │ │ ├── constants.js │ │ └── date.js ├── layouts │ ├── index.js │ ├── NoHeader │ │ ├── index.js │ │ └── styles.js │ ├── Header │ │ ├── index.js │ │ └── styles.js │ └── Master │ │ ├── styles.js │ │ └── index.js ├── setupTests.js ├── config │ ├── errors.js │ ├── sheet.js │ └── localStorage.js ├── utils │ ├── math.js │ └── login.js ├── screens │ ├── Main │ │ ├── styles.js │ │ └── index.js │ ├── index.js │ ├── Type │ │ ├── styles.js │ │ └── index.js │ ├── Data │ │ ├── styles.js │ │ ├── Chart.js │ │ ├── index.js │ │ └── Table.js │ ├── Splash │ │ ├── styles.js │ │ └── index.js │ ├── Onboarding │ │ └── styles.js │ ├── Config │ │ ├── styles.js │ │ ├── index.js │ │ └── schedule.js │ ├── Analytics │ │ ├── styles.js │ │ └── index.js │ └── Guide │ │ ├── styles.js │ │ └── index.js ├── components │ ├── Loading │ │ ├── styles.js │ │ └── index.js │ ├── Icons │ │ ├── Navigate.js │ │ ├── Delete.js │ │ ├── Close.js │ │ ├── Menu.js │ │ ├── Info.js │ │ ├── Home.js │ │ ├── MenuSearch.js │ │ ├── Search.js │ │ ├── index.js │ │ ├── PieChart.js │ │ ├── SignOut.js │ │ ├── Calendar.js │ │ ├── Dollar.js │ │ ├── ArrowIndicator.js │ │ ├── LineChart.js │ │ └── Gear.js │ ├── Dropdown │ │ ├── index.js │ │ └── styles.js │ ├── Button │ │ ├── index.js │ │ └── styles.js │ ├── Modal │ │ ├── index.js │ │ └── styles.js │ ├── Tabs │ │ ├── index.js │ │ └── styles.js │ ├── HeaderInfo │ │ ├── styles.js │ │ └── index.js │ ├── BigModal │ │ ├── index.js │ │ └── styles.js │ ├── Footer │ │ ├── styles.js │ │ └── index.js │ ├── Checkbox │ │ ├── index.js │ │ └── styles.js │ ├── BarChart │ │ ├── styles.js │ │ └── index.js │ ├── MonthLegend │ │ ├── index.js │ │ └── styles.js │ ├── Tooltip │ │ ├── styles.js │ │ └── index.js │ ├── RadioButton │ │ ├── index.js │ │ └── styles.js │ ├── Header │ │ ├── styles.js │ │ └── index.js │ ├── LineChart │ │ ├── styles.js │ │ └── index.js │ ├── HeaderBox │ │ ├── index.js │ │ └── styles.js │ ├── ChartLegend │ │ ├── styles.js │ │ └── index.js │ ├── index.js │ ├── DetailItem │ │ ├── styles.js │ │ └── index.js │ ├── Search │ │ ├── styles.js │ │ └── index.js │ ├── Input │ │ ├── index.js │ │ └── styles.js │ └── PieChart │ │ └── index.js ├── App.js ├── index.js ├── index.css ├── router │ └── index.js └── serviceWorker.js ├── babel.config.json ├── jsconfig.json ├── .gitignore ├── package.json └── README.md /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /src/context/types/Global.js: -------------------------------------------------------------------------------- 1 | export const RESET = "RESET"; 2 | -------------------------------------------------------------------------------- /src/services/index.js: -------------------------------------------------------------------------------- 1 | export * from "./spreadsheet"; 2 | export * from "./auth"; 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/img/Arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/public/img/Arch.png -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/rsc/img/emptybox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/src/rsc/img/emptybox.png -------------------------------------------------------------------------------- /public/img/screens/Arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/public/img/screens/Arch.png -------------------------------------------------------------------------------- /public/img/screens/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/public/img/screens/home.png -------------------------------------------------------------------------------- /src/rsc/img/iosInstall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/src/rsc/img/iosInstall.png -------------------------------------------------------------------------------- /public/img/screens/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/public/img/screens/cover.png -------------------------------------------------------------------------------- /public/img/screens/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/public/img/screens/splash.png -------------------------------------------------------------------------------- /public/img/screens/types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/public/img/screens/types.png -------------------------------------------------------------------------------- /public/img/spreadsheetSS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/public/img/spreadsheetSS.png -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env"], 3 | "plugins": ["@babel/plugin-proposal-export-namespace-from"] 4 | } 5 | -------------------------------------------------------------------------------- /public/img/screens/analytics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/public/img/screens/analytics.png -------------------------------------------------------------------------------- /src/rsc/img/androidInstall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/src/rsc/img/androidInstall.png -------------------------------------------------------------------------------- /src/rsc/img/loginScreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/src/rsc/img/loginScreenshot.png -------------------------------------------------------------------------------- /public/img/screens/onboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/public/img/screens/onboarding.png -------------------------------------------------------------------------------- /public/img/screens/home_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/public/img/screens/home_success.png -------------------------------------------------------------------------------- /public/img/screens/types_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/public/img/screens/types_details.png -------------------------------------------------------------------------------- /src/rsc/img/spreadsheetScreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoaquinBeceiro/bills-tracker/HEAD/src/rsc/img/spreadsheetScreenshot.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "baseUrl": "./src" 5 | }, 6 | "include": ["./src"] 7 | } 8 | -------------------------------------------------------------------------------- /src/context/types/Modal.js: -------------------------------------------------------------------------------- 1 | export const MODAL_SHOW = "MODAL_SHOW"; 2 | export const MODAL_HIDE = "MODAL_HIDE"; 3 | export const MODAL_RESET = "MODAL_RESET"; 4 | -------------------------------------------------------------------------------- /src/lib/utils/colors.js: -------------------------------------------------------------------------------- 1 | import { COLORS } from "./constants"; 2 | 3 | export const addColors = (types) => 4 | types.map((e, idx) => ({ ...e, color: COLORS[idx % COLORS.length] })); 5 | -------------------------------------------------------------------------------- /src/layouts/index.js: -------------------------------------------------------------------------------- 1 | export { default as NoHeaderLayout } from "./NoHeader"; 2 | export { default as MasterLayout } from "./Master"; 3 | export { default as HeaderLayout } from "./Header"; 4 | -------------------------------------------------------------------------------- /src/context/types/Search.js: -------------------------------------------------------------------------------- 1 | export const SEARCH_SHOW = "SEARCH_SHOW"; 2 | export const SEARCH_HIDE = "SEARCH_HIDE"; 3 | export const SEARCH_RESET = "SEARCH_RESET"; 4 | export const SEARCH_INPUT = "SEARCH_INPUT"; 5 | -------------------------------------------------------------------------------- /src/layouts/NoHeader/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Content } from "./styles"; 3 | 4 | const Onboarding = ({ children }) => { 5 | return {children}; 6 | }; 7 | 8 | export default Onboarding; 9 | -------------------------------------------------------------------------------- /src/context/types/index.js: -------------------------------------------------------------------------------- 1 | import * as User from "./User"; 2 | import * as Global from "./Global"; 3 | import * as Modal from "./Modal"; 4 | import * as Search from "./Search"; 5 | 6 | export default { 7 | User, 8 | Global, 9 | Modal, 10 | Search 11 | }; 12 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/lib/utils/toast.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { toast } from "react-toastify"; 3 | 4 | const success = (message) => toast.success(<>{message}); 5 | const error = (message) => toast.error(<>{message}); 6 | const dark = (message) => toast.dark(<>{message}); 7 | 8 | export default { success, error, dark }; 9 | -------------------------------------------------------------------------------- /src/config/errors.js: -------------------------------------------------------------------------------- 1 | const authErrors = { 2 | idpiframe_initialization_failed: 3 | "There was an error trying to authenticate the origin with Google Srvices.", 4 | default: "There was an error trying to authenticate with Google Srvices.", 5 | }; 6 | 7 | export const getAuthErrorMessage = (code) => 8 | authErrors[code] || authErrors["default"]; 9 | -------------------------------------------------------------------------------- /src/utils/math.js: -------------------------------------------------------------------------------- 1 | import Mexp from "math-expression-evaluator"; 2 | 3 | export const isMath = (string) => { 4 | return /\d+(\+|-|\*|\/)\d+/.test(string) 5 | } 6 | 7 | export const evaluateMath = (string) => { 8 | const mexp = new Mexp(); 9 | return mexp.eval(string) 10 | } 11 | export const isNumber = (string) => { 12 | return /^[0-9]+$/.test(string) 13 | } -------------------------------------------------------------------------------- /src/context/types/User.js: -------------------------------------------------------------------------------- 1 | export const SET_USER_START = "SET_USER_START"; 2 | export const SET_USER_SUCCESS = "SET_USER_SUCCESS"; 3 | export const SET_USER_FINISH = "SET_USER_FINISH"; 4 | export const SET_APP = "SET_APP"; 5 | 6 | export const GET_DOC_START = "GET_DOC_START"; 7 | export const GET_DOC_SUCCESS = "GET_DOC_SUCCESS"; 8 | export const SET_DOC_ERROR = "SET_DOC_ERROR"; 9 | -------------------------------------------------------------------------------- /src/lib/utils/index.js: -------------------------------------------------------------------------------- 1 | import * as Constants from "./constants"; 2 | import * as Currency from "./currency"; 3 | import * as Date from "./date"; 4 | import * as Toast from "./toast"; 5 | import * as Colors from "./colors"; 6 | import * as Common from "./common"; 7 | 8 | export default { 9 | Constants, 10 | Currency, 11 | Date, 12 | Toast, 13 | Colors, 14 | Common, 15 | }; 16 | -------------------------------------------------------------------------------- /src/screens/Main/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const ShowTable = styled.div` 4 | position: fixed; 5 | bottom: 0; 6 | left: 0; 7 | width: 100%; 8 | height: 50px; 9 | background-color: #000; 10 | color: #fff; 11 | text-align: center; 12 | font-weight: bolder; 13 | padding: 5px; 14 | cursor: pointer; 15 | `; 16 | 17 | export { ShowTable }; 18 | -------------------------------------------------------------------------------- /src/screens/index.js: -------------------------------------------------------------------------------- 1 | export { default as MainScreen } from "./Main"; 2 | export { default as TypeScreen } from "./Type"; 3 | export { default as AnalyticsScreen } from "./Analytics"; 4 | export { default as OnboardingScreen } from "./Onboarding"; 5 | export { default as SplashScreen } from "./Splash"; 6 | export { default as GuideScreen } from "./Guide"; 7 | export { default as ConfigScreen } from "./Config"; 8 | -------------------------------------------------------------------------------- /src/layouts/NoHeader/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Content = styled.div` 4 | margin-top: 5px; 5 | background: #ffffff; 6 | border-radius: 30px 30px 0px 0px; 7 | flex: 1; 8 | width: 100%; 9 | padding: 30px; 10 | display: flex; 11 | flex-direction: column; 12 | justify-content: space-between; 13 | overflow-y: auto; 14 | `; 15 | 16 | export { Content }; 17 | -------------------------------------------------------------------------------- /src/layouts/Header/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Container, Content } from "./styles"; 3 | import { HeaderInfoComponent } from "components"; 4 | 5 | const Header = ({ children, headerBox }) => { 6 | return ( 7 | 8 | 9 | {children} 10 | 11 | ); 12 | }; 13 | 14 | export default Header; 15 | -------------------------------------------------------------------------------- /src/components/Loading/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Container = styled.div` 4 | position: fixed; 5 | top: 0px; 6 | left: 0px; 7 | height: 100vh; 8 | width: 100%; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | background-color: #00000050; 13 | flex-direction: column; 14 | z-index: 9999; 15 | `; 16 | 17 | export { Container }; 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .netlify 25 | 26 | .env -------------------------------------------------------------------------------- /src/components/Icons/Navigate.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Navigate = () => ( 4 | 11 | 15 | 16 | ); 17 | 18 | export default Navigate; 19 | -------------------------------------------------------------------------------- /src/components/Icons/Delete.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Delete = () => ( 4 | 5 | 6 | 7 | 8 | 9 | ); 10 | 11 | export default Delete; 12 | -------------------------------------------------------------------------------- /src/components/Loading/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Lottie from "react-lottie-player"; 3 | import bouncingCoin from "../../rsc/lottie/bouncingCoin.json"; 4 | 5 | import { Container } from "./styles"; 6 | 7 | const Loading = () => { 8 | return ( 9 | 10 | 15 | 16 | ); 17 | }; 18 | 19 | export default Loading; 20 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Router from "router"; 3 | import AppContextProvider from "context"; 4 | import { GoogleOAuthProvider } from "@react-oauth/google"; 5 | 6 | function App() { 7 | const clientID = process.env.REACT_APP_GOOGLE_OAUTH_CLIENT_ID; 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /src/components/Icons/Close.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Close = () => ( 4 | 11 | 15 | 16 | ); 17 | 18 | export default Close; 19 | -------------------------------------------------------------------------------- /src/components/Icons/Menu.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Menu = () => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default Menu; 10 | -------------------------------------------------------------------------------- /src/rsc/img/bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/Dropdown/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from "./styles"; 3 | 4 | const Dropdown = ({ 5 | type = "default", 6 | placeholder, 7 | options, 8 | onChange, 9 | disabled, 10 | value, 11 | }) => { 12 | const defaultProps = { 13 | onChange, 14 | disabled, 15 | }; 16 | 17 | return { 18 | default: ( 19 | 25 | ), 26 | }[type]; 27 | }; 28 | 29 | export default Dropdown; 30 | -------------------------------------------------------------------------------- /src/components/Button/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from "./styles"; 3 | 4 | const Button = ({ type = "default", text, action, disabled }) => { 5 | const handleClick = () => { 6 | action && action(); 7 | }; 8 | 9 | const defaultProps = { 10 | onClick: handleClick, 11 | disabled, 12 | }; 13 | 14 | return { 15 | default: {text}, 16 | secondary: {text}, 17 | text: {text}, 18 | }[type]; 19 | }; 20 | 21 | export default Button; 22 | -------------------------------------------------------------------------------- /src/screens/Type/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | width: 100%; 5 | padding-bottom: 50px; 6 | `; 7 | 8 | export const TitleContainer = styled.div` 9 | display: flex; 10 | justify-content: space-between; 11 | > div { 12 | flex: 1; 13 | :last-child { 14 | display: flex; 15 | } 16 | } 17 | `; 18 | 19 | export const Title = styled.h2` 20 | font-family: Roboto; 21 | font-style: normal; 22 | font-weight: normal; 23 | font-size: 18px; 24 | `; 25 | 26 | 27 | export const ShowAllContainer = styled.div` 28 | margin-top: 10px; 29 | `; 30 | -------------------------------------------------------------------------------- /src/components/Modal/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from "./styles"; 3 | import { ButtonComponent } from "components"; 4 | 5 | const Modal = ({ title, content, actions }) => { 6 | return ( 7 | 8 | 9 | {title} 10 | {content} 11 | 12 | {actions.map((action, key) => ( 13 | 14 | ))} 15 | 16 | 17 | 18 | ); 19 | }; 20 | export default Modal; 21 | -------------------------------------------------------------------------------- /src/components/Tabs/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from "./styles"; 3 | import clsx from "clsx"; 4 | 5 | const Tabs = ({ items, action }) => { 6 | return ( 7 | 8 | 9 | {items.map(({ label, active, disabled }) => ( 10 | !disabled && action(label)} 14 | > 15 | {label} 16 | 17 | ))} 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default Tabs; 24 | -------------------------------------------------------------------------------- /src/components/HeaderInfo/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | max-width: 100%; 5 | .carousel-slider { 6 | padding-bottom: 15px; 7 | } 8 | .control-dots { 9 | display: flex; 10 | justify-content: center; 11 | margin-top: 10px; 12 | bottom: -10px; 13 | } 14 | `; 15 | 16 | export const Indicator = styled.li` 17 | width: ${({ isSelected }) => (isSelected ? "13.6px" : "6.8px")}; 18 | height: 6.8px; 19 | background: ${({ isSelected }) => (isSelected ? "#fff" : "#d9d9d9")}; 20 | border-radius: 6px; 21 | margin: 3px; 22 | transition: all 0.5s ease; 23 | `; 24 | -------------------------------------------------------------------------------- /src/components/Icons/Info.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Info = () => ( 4 | 11 | 15 | 19 | 20 | ); 21 | 22 | export default Info; 23 | -------------------------------------------------------------------------------- /src/config/sheet.js: -------------------------------------------------------------------------------- 1 | const sheetHeaders = ["Date", "Who", "Amount", "Type", "Detail"]; 2 | const sheetHeadersConfig = [ 3 | "Name", 4 | "Type", 5 | "Frequency", 6 | "Amount", 7 | "Description", 8 | "Date", 9 | ]; 10 | const defaultTypes = ["Supermercado", "Farmacia", "Vestimenta", "Otros"]; 11 | const docName = "BillsTracker"; 12 | const sheetTitle = "Bills"; 13 | const sheetTitleConfig = "Config"; 14 | const sheetScope = "profile email https://www.googleapis.com/auth/spreadsheets"; 15 | 16 | export { 17 | sheetHeaders, 18 | defaultTypes, 19 | docName, 20 | sheetTitle, 21 | sheetScope, 22 | sheetTitleConfig, 23 | sheetHeadersConfig, 24 | }; 25 | -------------------------------------------------------------------------------- /src/screens/Data/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const AbsoluteContainerStyled = styled.div` 4 | position: absolute; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | background-color: #eee; 9 | top: 0; 10 | bottom: 0; 11 | left: 0; 12 | width: 100%; 13 | font-size: 10px; 14 | & .close { 15 | position: fixed; 16 | bottom: 0; 17 | width: 100%; 18 | height: 50px; 19 | background-color: #000; 20 | color: #fff; 21 | text-align: center; 22 | font-weight: bolder; 23 | padding: 5px; 24 | cursor: pointer; 25 | } 26 | `; 27 | 28 | export { AbsoluteContainerStyled }; 29 | -------------------------------------------------------------------------------- /src/components/BigModal/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from "./styles"; 3 | 4 | const BigModal = ({ title, subTitle, children, handleClose }) => { 5 | return ( 6 | 7 | e.preventDefault()} > 8 | { 9 | subTitle ? 10 | 11 | {title} 12 | {subTitle} 13 | : 14 | {title} 15 | } 16 | {children} 17 | 18 | 19 | ); 20 | }; 21 | export default BigModal; 22 | -------------------------------------------------------------------------------- /src/components/Footer/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Container = styled.div` 4 | position: fixed; 5 | bottom: 0; 6 | height: 60px; 7 | width: 100%; 8 | max-width: inherit; 9 | background: #ffffff; 10 | box-shadow: 0px -1px 4px rgba(196, 196, 196, 0.5); 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | `; 15 | 16 | const MenuItem = styled.button` 17 | border: none; 18 | background: none; 19 | display: flex; 20 | flex: 1; 21 | justify-content: center; 22 | align-items: center; 23 | height: 100%; 24 | path { 25 | fill: #aeaeae; 26 | } 27 | &.active path { 28 | fill: #000; 29 | } 30 | `; 31 | 32 | export { Container, MenuItem }; 33 | -------------------------------------------------------------------------------- /src/components/Checkbox/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from "./styles"; 3 | import { v4 as uuidv4 } from "uuid"; 4 | 5 | const Checkbox = ({ label, checked, onChange, disabled, color }) => { 6 | const id = uuidv4(); 7 | return ( 8 | 9 | 10 | 19 | 20 | {label} 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default Checkbox; 27 | -------------------------------------------------------------------------------- /src/layouts/Header/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Container = styled.div` 4 | display: flex; 5 | flex: 1; 6 | flex-direction: column; 7 | align-items: center; 8 | max-height: 100%; 9 | max-width: 100%; 10 | `; 11 | 12 | const Content = styled.div` 13 | margin-top: 5px; 14 | background: #ffffff; 15 | border-radius: 30px 30px 0px 0px; 16 | width: 100%; 17 | padding: 25px 30px 80px 30px; 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: space-between; 21 | height: 100%; 22 | overflow-y: auto; 23 | margin-bottom: 0px; 24 | min-height: calc(100vh - 190px); 25 | @media (display-mode: browser) { 26 | min-height: auto; 27 | } 28 | `; 29 | 30 | export { Container, Content }; 31 | -------------------------------------------------------------------------------- /src/lib/utils/common.js: -------------------------------------------------------------------------------- 1 | import Utils from "lib/utils"; 2 | 3 | export const isJsonString = (str) => { 4 | try { 5 | JSON.parse(str); 6 | } catch (e) { 7 | return false; 8 | } 9 | return true; 10 | }; 11 | 12 | export const getSpreadsheetId = (url) => { 13 | if (url) { 14 | if (url.slice(0, 4) === "http") { 15 | const matches = /\/([\w-_]{18,})(.*?gid=(\d+))?/.exec(url); 16 | return matches[1]; 17 | } else { 18 | return url; 19 | } 20 | } else { 21 | return null; 22 | } 23 | }; 24 | 25 | export const getTotalRecords = records => { 26 | const { moneyToNumber, formatMoney } = Utils.Currency; 27 | const total = records.reduce((prev, cur) => prev + moneyToNumber(cur.Amount), 0); 28 | return formatMoney(total); 29 | } -------------------------------------------------------------------------------- /src/context/reducers/Search.js: -------------------------------------------------------------------------------- 1 | import { DispatchTypes } from "../"; 2 | 3 | export const searchInitialState = { 4 | show: false, 5 | input: "", 6 | }; 7 | 8 | const SearchReducer = (currentState, action) => { 9 | switch (action.type) { 10 | case DispatchTypes.Search.SEARCH_INPUT: 11 | currentState.input = action.input; 12 | return { ...currentState }; 13 | case DispatchTypes.Search.SEARCH_SHOW: 14 | currentState.show = true; 15 | return { ...currentState }; 16 | case DispatchTypes.Search.SEARCH_HIDE: 17 | currentState.show = false; 18 | return { ...currentState }; 19 | case DispatchTypes.Search.SEARCH_RESET: 20 | return searchInitialState; 21 | default: 22 | return currentState; 23 | } 24 | }; 25 | 26 | export default SearchReducer; 27 | -------------------------------------------------------------------------------- /src/components/Icons/Home.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Home = () => ( 4 | 11 | 15 | 19 | 20 | ); 21 | 22 | export default Home; 23 | -------------------------------------------------------------------------------- /src/components/Icons/MenuSearch.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const MenuSearch = () => ( 4 | 11 | 15 | 16 | ); 17 | 18 | export default MenuSearch; 19 | -------------------------------------------------------------------------------- /src/components/Icons/Search.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Search = () => ( 4 | 11 | 15 | 16 | ); 17 | 18 | export default Search; 19 | -------------------------------------------------------------------------------- /src/context/reducers/Modal.js: -------------------------------------------------------------------------------- 1 | import { DispatchTypes } from "../"; 2 | 3 | export const modalInitialState = { 4 | show: false, 5 | title: "", 6 | content: "", 7 | actions: [], 8 | }; 9 | 10 | const ModalReducer = (currentState, action) => { 11 | switch (action.type) { 12 | case DispatchTypes.Modal.MODAL_SHOW: 13 | currentState.show = true; 14 | currentState.title = action.title; 15 | currentState.content = action.content; 16 | currentState.actions = action.actions; 17 | return { ...currentState }; 18 | case DispatchTypes.Modal.MODAL_HIDE: 19 | currentState.show = false; 20 | return { ...currentState }; 21 | case DispatchTypes.Modal.MODAL_RESET: 22 | return modalInitialState; 23 | default: 24 | return currentState; 25 | } 26 | }; 27 | 28 | export default ModalReducer; 29 | -------------------------------------------------------------------------------- /src/components/Icons/index.js: -------------------------------------------------------------------------------- 1 | export { default as SignOutIcon } from "./SignOut"; 2 | export { default as PieChartIcon } from "./PieChart"; 3 | export { default as LineChartIcon } from "./LineChart"; 4 | export { default as HomeIcon } from "./Home"; 5 | export { default as ArrowIndicatorIcon } from "./ArrowIndicator"; 6 | export { default as DollarIcon } from "./Dollar"; 7 | export { default as CalendarIcon } from "./Calendar"; 8 | export { default as NavigateIcon } from "./Navigate"; 9 | export { default as DeleteIcon } from "./Delete"; 10 | export { default as InfoIcon } from "./Info"; 11 | export { default as MenuIcon } from "./Menu"; 12 | export { default as GearIcon } from "./Gear"; 13 | export { default as SearchIcon } from "./Search"; 14 | export { default as MenuSearchIcon } from "./MenuSearch"; 15 | export { default as CloseIcon } from "./Close"; 16 | -------------------------------------------------------------------------------- /src/components/BarChart/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Tooltip = styled.div` 4 | background: #ffffff; 5 | border-radius: 4px; 6 | padding: 9px 4px; 7 | filter: drop-shadow(0px 0px 7px rgba(0, 0, 0, 0.25)); 8 | min-width: 50px; 9 | text-align: center; 10 | font-family: Roboto; 11 | font-style: normal; 12 | font-weight: 300; 13 | font-size: 11px; 14 | :before { 15 | content: ""; 16 | position: absolute; 17 | left: calc(50% - 8px); 18 | bottom: -8px; 19 | width: 0; 20 | height: 0; 21 | border-left: 8px solid transparent; 22 | border-right: 8px solid transparent; 23 | border-top: 8px solid #fff; 24 | } 25 | `; 26 | 27 | export const LoadingContainer = styled.div` 28 | display: flex; 29 | justify-content: center; 30 | align-items: center; 31 | height: 100%; 32 | `; 33 | -------------------------------------------------------------------------------- /src/components/Tabs/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | width: 100%; 6 | `; 7 | 8 | export const Menu = styled.ul` 9 | display: flex; 10 | justify-content: space-between; 11 | width: 100%; 12 | padding: 0; 13 | margin: 0; 14 | `; 15 | 16 | export const MenuItem = styled.li` 17 | display: flex; 18 | color: #444; 19 | font-family: Roboto; 20 | font-size: 14px; 21 | font-style: normal; 22 | font-weight: 300; 23 | line-height: normal; 24 | text-transform: uppercase; 25 | text-underline-offset: 4px; 26 | cursor: pointer; 27 | 28 | &.active { 29 | color: #000; 30 | font-weight: 400; 31 | text-decoration: underline; 32 | } 33 | 34 | &.disabled { 35 | color: #ccc; 36 | font-weight: 300; 37 | cursor: not-allowed; 38 | } 39 | `; 40 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | import "typeface-roboto"; 7 | 8 | let vh = Math.abs(window.visualViewport.height); 9 | document.documentElement.style.setProperty("--vh", `${vh}px`); 10 | window.addEventListener("resize", () => { 11 | document.documentElement.style.setProperty("--vh", `${vh}px`); 12 | }); 13 | // Then we set the value in the --vh custom property to the root of the document 14 | 15 | ReactDOM.render(, document.getElementById("root")); 16 | 17 | // If you want your app to work offline and load faster, you can change 18 | // unregister() to register() below. Note this comes with some pitfalls. 19 | // Learn more about service workers: https://bit.ly/CRA-PWA 20 | serviceWorker.register(); 21 | -------------------------------------------------------------------------------- /src/screens/Splash/styles.js: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from "styled-components"; 2 | 3 | const fadeIn = keyframes` 4 | 0% { 5 | opacity: 0; 6 | } 7 | 100% { 8 | opacity: 1; 9 | } 10 | `; 11 | 12 | const Container = styled.div` 13 | display: flex; 14 | background: linear-gradient( 15 | 180deg, 16 | rgba(255, 255, 255, 0.1) 0%, 17 | rgba(255, 255, 255, 0) 100% 18 | ), 19 | #38b44e; 20 | width: 100%; 21 | min-height: 100vh; 22 | justify-content: center; 23 | align-items: center; 24 | flex-direction: column; 25 | color: white; 26 | `; 27 | 28 | const Content = styled.div` 29 | animation: 2s ${fadeIn} ease-out; 30 | `; 31 | 32 | const Title = styled.h1` 33 | font-family: Roboto; 34 | font-style: normal; 35 | font-weight: 500; 36 | font-size: 36px; 37 | `; 38 | 39 | export { Container, Content, Title }; 40 | -------------------------------------------------------------------------------- /src/components/MonthLegend/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as S from "./styles"; 3 | import Utils from "lib/utils"; 4 | import { CalendarIcon, NavigateIcon } from "components"; 5 | 6 | const MonthLegend = ({ month, year, amount, action }) => { 7 | const { formatMoney } = Utils.Currency; 8 | const { monthToText } = Utils.Date; 9 | 10 | const monthText = `${monthToText(month - 1).substring(0, 3)}.`; 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | {monthText}, {year} 19 | 20 | {`$${formatMoney(amount)}`} 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default MonthLegend; 29 | -------------------------------------------------------------------------------- /src/lib/utils/currency.js: -------------------------------------------------------------------------------- 1 | export const moneyToNumber = (value) => 2 | parseInt(value.replace("$", "").replace(".", "").replace(",", "")); 3 | 4 | export const formatMoney = (value) => { 5 | return Number(value.toFixed(1)).toLocaleString(); 6 | }; 7 | 8 | export const formatSymbol = (num, digits = 1) => { 9 | const lookup = [ 10 | { value: 1, symbol: "" }, 11 | { value: 1e3, symbol: "k" }, 12 | { value: 1e6, symbol: "M" }, 13 | { value: 1e9, symbol: "G" }, 14 | { value: 1e12, symbol: "T" }, 15 | { value: 1e15, symbol: "P" }, 16 | { value: 1e18, symbol: "E" }, 17 | ]; 18 | const rx = /\.0+$|(\.[0-9]*[1-9])0+$/; 19 | var item = lookup 20 | .slice() 21 | .reverse() 22 | .find(function (item) { 23 | return num >= item.value; 24 | }); 25 | return item 26 | ? (num / item.value).toFixed(digits).replace(rx, "$1") + item.symbol 27 | : "0"; 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/Tooltip/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | position: relative; 5 | `; 6 | 7 | export const Tooltip = styled.div` 8 | display: ${({ open }) => (open ? "block" : "none")}; 9 | position: absolute; 10 | background-color: #fff; 11 | padding: 5px 4px; 12 | border-radius: 4px; 13 | left: 100%; 14 | top: 0; 15 | margin-left: 5px; 16 | width: 100%; 17 | min-width: 140px; 18 | box-shadow: 0px 0px 7px 0px rgba(0, 0, 0, 0.25); 19 | 20 | h2 { 21 | color: #000; 22 | text-align: center; 23 | font-size: 12px; 24 | font-family: Roboto; 25 | font-weight: 600; 26 | margin: 0 0 3px 0; 27 | } 28 | 29 | p { 30 | color: #000; 31 | font-size: 12px; 32 | font-family: Roboto; 33 | font-weight: 300; 34 | margin: 0; 35 | line-height: 12px; 36 | text-align: center; 37 | } 38 | `; 39 | -------------------------------------------------------------------------------- /src/components/RadioButton/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import * as S from "./styles"; 3 | 4 | const RadioButton = ({ options, selected, onChange, disabled, name }) => { 5 | return ( 6 | 7 | {options.map(({ label, value }) => ( 8 | 9 | 10 | 19 | 20 | {label} 21 | 22 | 23 | ))} 24 | 25 | ); 26 | }; 27 | 28 | export default RadioButton; 29 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 8 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 9 | sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | background-color: #38b44e; 13 | } 14 | 15 | @media (display-mode: browser) { 16 | 17 | body, 18 | #root { 19 | height: var(--vh); 20 | max-height: var(--vh); 21 | overflow-y: hidden; 22 | } 23 | 24 | } 25 | 26 | #root { 27 | display: flex; 28 | justify-content: center; 29 | } 30 | 31 | #root>div { 32 | width: 100%; 33 | max-width: 700px; 34 | } 35 | 36 | code { 37 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 38 | monospace; 39 | } 40 | 41 | .MuiTablePagination-root { 42 | font-size: 10px !important; 43 | } -------------------------------------------------------------------------------- /src/screens/Onboarding/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Content = styled.div` 4 | height: calc(100vh - 125px); 5 | @media (display-mode: browser) { 6 | height: 100%; 7 | } 8 | display: flex; 9 | flex-direction: column; 10 | > div:first-child { 11 | flex: 1; 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | } 16 | .googleButton { 17 | height: 60px; 18 | margin-top: 40px; 19 | span { 20 | text-align: center; 21 | width: 100%; 22 | color: #757575; 23 | font-weight: 500; 24 | font-size: 14px; 25 | } 26 | } 27 | 28 | .buttonDisabled { 29 | span { 30 | opacity: 0.4; 31 | } 32 | } 33 | `; 34 | 35 | export const GoogleDisclaimer = styled.p` 36 | margin-top: 30px; 37 | color: #aeaeae; 38 | font-weight: 400; 39 | font-size: 12px; 40 | text-align: center; 41 | `; 42 | -------------------------------------------------------------------------------- /src/components/Button/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Button = styled.button` 4 | border-radius: 15px; 5 | width: 100%; 6 | height: 60px; 7 | border: none; 8 | color: white; 9 | text-transform: uppercase; 10 | font-family: Roboto; 11 | font-style: normal; 12 | font-weight: 500; 13 | font-size: 18px; 14 | cursor: pointer; 15 | &:focus { 16 | outline: none; 17 | } 18 | &:disabled { 19 | background-color: #c4c4c4; 20 | cursor: not-allowed; 21 | } 22 | `; 23 | 24 | export const Default = styled(Button)` 25 | background: #38b44e; 26 | &:hover { 27 | background-color: #6fc97f; 28 | } 29 | `; 30 | 31 | export const Secondary = styled(Button)` 32 | background: #333333; 33 | color: #ffffff; 34 | &:hover { 35 | background-color: #6fc97f; 36 | } 37 | `; 38 | 39 | export const Text = styled(Button)` 40 | background: none; 41 | color: #333; 42 | `; 43 | -------------------------------------------------------------------------------- /src/components/MonthLegend/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | background: #ffffff; 6 | box-shadow: 0px 0px 7px rgba(0, 0, 0, 0.25); 7 | border-radius: 6px; 8 | padding: 12px 10px; 9 | margin-bottom: 17px; 10 | `; 11 | 12 | export const IconContainer = styled.div``; 13 | 14 | export const DateContainer = styled.div` 15 | flex: 1; 16 | display: flex; 17 | align-items: center; 18 | font-family: Roboto; 19 | font-weight: 300; 20 | font-size: 12px; 21 | color: #333333; 22 | padding: 0 8px; 23 | `; 24 | 25 | export const AmountContainer = styled.div` 26 | display: flex; 27 | align-items: center; 28 | color: #5455ff; 29 | font-family: Roboto; 30 | font-weight: normal; 31 | font-size: 18px; 32 | `; 33 | 34 | export const NavigateContainer = styled.div` 35 | display: flex; 36 | align-items: center; 37 | padding-left: 8px; 38 | `; 39 | -------------------------------------------------------------------------------- /src/services/auth.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const baseURL = process.env.REACT_APP_BASE_URI; 4 | 5 | export const getRefreshToken = async (code) => { 6 | try { 7 | const { data } = await axios.get(`${baseURL}/oauth2callback?code=${code}`); 8 | return data; 9 | } catch (error) { 10 | console.log("ERROR:: ", error); 11 | return null; 12 | } 13 | }; 14 | 15 | export const getNewTokens = async ({ 16 | access_token, 17 | expiry_date, 18 | id_token, 19 | refresh_token, 20 | scope, 21 | token_type, 22 | }) => { 23 | try { 24 | const tokens = { 25 | access_token, 26 | expiry_date, 27 | id_token, 28 | refresh_token, 29 | scope, 30 | token_type, 31 | }; 32 | const { data } = await axios.get(`${baseURL}/refresh`, { 33 | params: { tokens }, 34 | }); 35 | return data; 36 | } catch (error) { 37 | console.log("ERROR: ", error); 38 | return null; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/Header/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Main = styled.div` 4 | height: 60px; 5 | color: white; 6 | display: flex; 7 | align-items: center; 8 | top: 0px; 9 | z-index: 999; 10 | width: 100%; 11 | ${({ colorChange }) => colorChange && `background-color: #38b44e;`} 12 | transition: background-color 0.5s ease; 13 | `; 14 | 15 | const TitleContainer = styled.div` 16 | display: flex; 17 | flex-direction: column; 18 | flex: 1; 19 | text-align: center; 20 | h1 { 21 | font-weight: bold; 22 | font-size: 18px; 23 | margin: 0; 24 | } 25 | 26 | h2 { 27 | font-weight: normal; 28 | font-size: 13px; 29 | margin: 0; 30 | } 31 | `; 32 | 33 | const ActionContainer = styled.div` 34 | width: 50px; 35 | height: 100%; 36 | display: flex; 37 | justify-content: center; 38 | align-items: center; 39 | svg path { 40 | fill: white; 41 | } 42 | `; 43 | 44 | export { Main, TitleContainer, ActionContainer }; 45 | -------------------------------------------------------------------------------- /src/components/LineChart/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Tooltip = styled.div` 4 | background: #ffffff; 5 | border-radius: 4px; 6 | padding: 9px 4px; 7 | filter: drop-shadow(0px 0px 7px rgba(0, 0, 0, 0.25)); 8 | min-width: 50px; 9 | text-align: center; 10 | font-family: Roboto; 11 | font-style: normal; 12 | font-weight: 300; 13 | font-size: 11px; 14 | p { 15 | margin: 0 0 10px 0; 16 | font-size: 16px; 17 | font-weight: 600; 18 | } 19 | ul { 20 | list-style: none; 21 | margin: 0; 22 | padding: 0; 23 | li { 24 | display: flex; 25 | justify-content: space-between; 26 | gap: 10px; 27 | span { 28 | font-weight: 300; 29 | :first-child { 30 | font-weight: 400; 31 | } 32 | } 33 | } 34 | } 35 | `; 36 | 37 | export const LoadingContainer = styled.div` 38 | display: flex; 39 | justify-content: center; 40 | align-items: center; 41 | height: 100%; 42 | `; 43 | -------------------------------------------------------------------------------- /src/layouts/Master/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import bg from "../../rsc/img/bg.svg"; 3 | 4 | const Container = styled.div` 5 | /* padding-bottom: ${({ footer }) => (footer ? "60px" : "0px;")}; */ 6 | display: flex; 7 | flex-direction: column; 8 | height: 100vh; 9 | min-height: 100vh; 10 | @media (display-mode: browser) { 11 | height: var(--vh); 12 | max-height: var(--vh); 13 | } 14 | background-color: #38b44e; 15 | z-index: 1; 16 | background: url(${bg}); 17 | background-size: cover; 18 | `; 19 | 20 | const Content = styled.div` 21 | display: flex; 22 | height: ${({ footer }) => 23 | footer ? "calc(var(--vh) - 110px)" : "calc(var(--vh) - 60px)"}; 24 | flex: 1; 25 | @media (display-mode: browser) { 26 | height: ${({ footer }) => 27 | footer ? "calc(var(--vh) - 120px)" : "calc(var(--vh) - 60px)"}; 28 | max-height: ${({ footer }) => 29 | footer ? "calc(var(--vh) - 120px)" : "calc(var(--vh) - 60px)"}; 30 | } 31 | `; 32 | 33 | export { Container, Content }; 34 | -------------------------------------------------------------------------------- /src/components/Modal/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | position: fixed; 6 | background-color: #00000035; 7 | justify-content: center; 8 | align-items: center; 9 | top: 0px; 10 | left: 0px; 11 | height: 100vh; 12 | width: 100vw; 13 | z-index: 999; 14 | `; 15 | 16 | export const Modal = styled.div` 17 | flex-direction: column; 18 | display: flex; 19 | background-color: #ffffff; 20 | padding: 23px; 21 | border-radius: 7px; 22 | max-width: 90vw; 23 | `; 24 | 25 | export const Title = styled.h3` 26 | text-align: center; 27 | `; 28 | 29 | export const Content = styled.div` 30 | text-align: center; 31 | `; 32 | 33 | export const ActionsContainer = styled.div` 34 | margin-top: 20px; 35 | display: flex; 36 | justify-content: center; 37 | flex-direction: column; 38 | align-items: center; 39 | button { 40 | margin: 5px 0; 41 | height: 30px; 42 | font-weight: 500; 43 | font-size: 12px; 44 | max-width: 150px; 45 | } 46 | `; 47 | -------------------------------------------------------------------------------- /src/components/Tooltip/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import { Container, Tooltip } from "./styles"; 3 | 4 | const CustomTooltip = ({ children, infoTitle, infoText }) => { 5 | const [open, setOpen] = useState(false); 6 | 7 | const ref = useRef(null); 8 | 9 | const handleOpen = () => { 10 | setOpen(!open); 11 | }; 12 | 13 | useEffect(() => { 14 | const handleClickOutside = (event) => { 15 | if (ref.current && !ref.current.contains(event.target)) { 16 | setOpen(false); 17 | } 18 | }; 19 | document.addEventListener("mousedown", handleClickOutside); 20 | return () => { 21 | document.removeEventListener("mousedown", handleClickOutside); 22 | }; 23 | }, []); 24 | 25 | return ( 26 | 27 | {children} 28 | 29 | {infoTitle &&

{infoTitle}

} 30 | {infoText &&

{infoText}

} 31 |
32 |
33 | ); 34 | }; 35 | 36 | export default CustomTooltip; 37 | -------------------------------------------------------------------------------- /src/config/localStorage.js: -------------------------------------------------------------------------------- 1 | export const getUserSession = () => JSON.parse(localStorage.getItem("user")); 2 | 3 | export const setUserSession = (user) => { 4 | const userFromStorage = getUserSession(); 5 | const newUser = { 6 | ...userFromStorage, 7 | ...user, 8 | }; 9 | if (userFromStorage?.name) { 10 | newUser.name = userFromStorage.name; 11 | } 12 | 13 | const userObject = JSON.stringify(newUser); 14 | localStorage.setItem("user", userObject); 15 | }; 16 | 17 | export const deleteUserSession = () => { 18 | localStorage.removeItem("data"); 19 | localStorage.removeItem("user"); 20 | }; 21 | 22 | export const setSheetData = (data) => { 23 | const dataObject = JSON.stringify(data); 24 | localStorage.setItem("data", dataObject); 25 | }; 26 | 27 | export const getSheetData = () => JSON.parse(localStorage.getItem("data")); 28 | 29 | export const setSheetConfig = (data) => { 30 | const dataObject = JSON.stringify(data); 31 | localStorage.setItem("config", dataObject); 32 | }; 33 | 34 | export const getSheetConfig = () => JSON.parse(localStorage.getItem("config")); 35 | -------------------------------------------------------------------------------- /src/screens/Config/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | width: 100%; 5 | padding: 20px 0 50px 0; 6 | flex: 1; 7 | `; 8 | 9 | export const Content = styled.div` 10 | display: flex; 11 | flex-direction: column; 12 | gap: 20px; 13 | height: 100%; 14 | `; 15 | 16 | export const TableContainer = styled.div` 17 | display: flex; 18 | flex-direction: column; 19 | flex: 1; 20 | `; 21 | 22 | export const Form = styled.div` 23 | display: flex; 24 | flex-direction: column; 25 | justify-content: space-between; 26 | gap: 20px; 27 | `; 28 | 29 | export const NoData = styled.div` 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | padding: 40px; 34 | text-align: center; 35 | flex-direction: column; 36 | color: #7e7e7e; 37 | font-family: Roboto; 38 | font-size: 13px; 39 | font-style: normal; 40 | font-weight: 300; 41 | 42 | img { 43 | width: 100%; 44 | } 45 | 46 | p { 47 | &.strong { 48 | font-weight: 900; 49 | } 50 | margin: 0; 51 | } 52 | `; 53 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Bills Tracker", 3 | "name": "Bills Tracker", 4 | "description": "Track and share expenses with a Google Drive Spreadsheet.", 5 | "orientation": "portrait", 6 | "display": "fullscreen", 7 | "icons": [ 8 | { 9 | "src": "favicon.ico", 10 | "sizes": "48x48", 11 | "type": "image/x-icon" 12 | }, 13 | { 14 | "src": "logo192.png", 15 | "type": "image/png", 16 | "sizes": "192x192" 17 | }, 18 | { 19 | "src": "logo512.png", 20 | "type": "image/png", 21 | "sizes": "512x512" 22 | } 23 | ], 24 | "start_url": ".", 25 | "theme_color": "#ffffff", 26 | "background_color": "#38B44E", 27 | "screenshots": [ 28 | { 29 | "src": "img/screens/cover.png", 30 | "sizes": "1080x1080", 31 | "type": "image/png", 32 | "form_factor": "narrow", 33 | "label": "Cover" 34 | }, 35 | { 36 | "src": "img/screens/home.png", 37 | "sizes": "360x640", 38 | "type": "image/png", 39 | "form_factor": "wide", 40 | "label": "Home" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /src/components/Icons/PieChart.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const PieChart = () => ( 4 | 11 | 15 | 16 | ); 17 | 18 | export default PieChart; 19 | -------------------------------------------------------------------------------- /src/components/Icons/SignOut.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const SignOut = () => ( 4 | 11 | 15 | 16 | ); 17 | 18 | export default SignOut; 19 | -------------------------------------------------------------------------------- /src/context/index.js: -------------------------------------------------------------------------------- 1 | import React, { useReducer, createContext } from "react"; 2 | import UserReducer, { userInitialState } from "./reducers/User"; 3 | import ModalReducer, { modalInitialState } from "./reducers/Modal"; 4 | import SearchReducer, { searchInitialState } from "./reducers/Search"; 5 | import Types from "./types"; 6 | 7 | export const GlobalContext = createContext(); 8 | 9 | export const DispatchTypes = Types; 10 | 11 | const AppContextProvider = ({ children }) => { 12 | const [userState, userDispatch] = useReducer(UserReducer, userInitialState); 13 | const [modalState, modalDispatch] = useReducer( 14 | ModalReducer, 15 | modalInitialState 16 | ); 17 | const [searchState, searchDispatch] = useReducer( 18 | SearchReducer, 19 | searchInitialState 20 | ); 21 | 22 | const values = { 23 | globalUser: [userState, userDispatch], 24 | globalModal: [modalState, modalDispatch], 25 | globalSearch: [searchState, searchDispatch], 26 | }; 27 | 28 | return ( 29 | {children} 30 | ); 31 | }; 32 | 33 | export default AppContextProvider; 34 | -------------------------------------------------------------------------------- /src/components/HeaderBox/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Container, 4 | Content, 5 | PrimaryValue, 6 | SecondaryValue, 7 | Title, 8 | Info, 9 | } from "./styles"; 10 | import { ArrowIndicatorIcon, InfoIcon, TooltipComponent } from "components"; 11 | 12 | const HeaderBox = ({ 13 | primaryValue, 14 | secondaryValue, 15 | title, 16 | arrowIcon, 17 | info, 18 | }) => { 19 | return ( 20 | 21 | 22 | {info && ( 23 | 24 | 25 | 26 | 27 | 28 | )} 29 | {title && {title}} 30 | {primaryValue && {primaryValue}} 31 | {secondaryValue && ( 32 | 33 | {arrowIcon && ( 34 | 35 | )} 36 | {secondaryValue} 37 | 38 | )} 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default HeaderBox; 45 | -------------------------------------------------------------------------------- /src/components/Icons/Calendar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Calendar = () => ( 4 | 11 | 18 | 25 | 32 | 39 | 40 | ); 41 | 42 | export default Calendar; 43 | -------------------------------------------------------------------------------- /src/screens/Analytics/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | width: 100%; 5 | padding-bottom: 50px; 6 | `; 7 | 8 | export const TitleContainer = styled.div` 9 | display: flex; 10 | justify-content: space-between; 11 | > div { 12 | flex: 1; 13 | :last-child { 14 | display: flex; 15 | } 16 | } 17 | `; 18 | 19 | export const Title = styled.h2` 20 | font-family: Roboto; 21 | font-style: normal; 22 | font-weight: normal; 23 | font-size: 18px; 24 | `; 25 | 26 | export const LeggendsContainer = styled.div` 27 | margin-top: 25px; 28 | `; 29 | 30 | export const SubTitleContainer = styled.div` 31 | margin: 20px 0px; 32 | `; 33 | 34 | export const SubTitle = styled.h3` 35 | font-family: Roboto; 36 | font-style: normal; 37 | font-weight: bold; 38 | font-size: 24px; 39 | margin: 0px; 40 | `; 41 | 42 | export const ResumeTotal = styled.h4` 43 | font-family: Roboto; 44 | font-weight: 300; 45 | font-size: 10px; 46 | span { 47 | font-weight: 500; 48 | color: #6fcf97; 49 | } 50 | `; 51 | 52 | export const CategoryList = styled.ul` 53 | margin: 0; 54 | padding: 0; 55 | list-style: none; 56 | display: flex; 57 | gap: 10px; 58 | flex-flow: row wrap; 59 | `; 60 | -------------------------------------------------------------------------------- /src/components/BigModal/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | position: fixed; 6 | background-color: #00000035; 7 | justify-content: center; 8 | align-items: flex-end; 9 | top: 0px; 10 | left: 0px; 11 | height: 100vh; 12 | width: 100vw; 13 | z-index: 1; 14 | `; 15 | 16 | export const Modal = styled.div` 17 | flex-direction: column; 18 | display: flex; 19 | background-color: #ffffff; 20 | padding: 23px 10px; 21 | border-radius: 30px 30px 0px 0px; 22 | width: 100vw; 23 | z-index: 2; 24 | max-height: 80vh; 25 | `; 26 | 27 | export const Title = styled.h3` 28 | text-align: center; 29 | margin-bottom: 30px; 30 | `; 31 | 32 | export const Content = styled.div` 33 | overflow-y: scroll; 34 | flex: 1; 35 | padding: 0 10px 20px 10px; 36 | `; 37 | 38 | export const TitleWithSubtitleContainer = styled.div` 39 | display: flex; 40 | justify-content: space-between; 41 | margin-bottom: 30px; 42 | `; 43 | 44 | export const TitleWithSubtitle = styled.div` 45 | text-align: left; 46 | font-weight: 400; 47 | font-size: 18px; 48 | `; 49 | 50 | export const SubTitle = styled.div` 51 | text-align: right; 52 | font-weight: 300; 53 | font-size: 18px; 54 | color: #7E7E7E; 55 | `; -------------------------------------------------------------------------------- /src/lib/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const RADIAN = Math.PI / 180; 2 | 3 | export const COLORS = [ 4 | "#f44336", 5 | "#9c27b0", 6 | "#3f51b5", 7 | "#03a9f4", 8 | "#009688", 9 | "#8bc34a", 10 | "#ffeb3b", 11 | "#ff9800", 12 | "#795548", 13 | "#607d8b", 14 | ]; 15 | 16 | export const MONTHS = [ 17 | "January", 18 | "February", 19 | "March", 20 | "April", 21 | "May", 22 | "June", 23 | "July", 24 | "August", 25 | "September", 26 | "October", 27 | "November", 28 | "December", 29 | ]; 30 | 31 | export const LAST_12_MONTHS_OPTION = { 32 | label: "Last 12M", 33 | value: "last12months", 34 | }; 35 | 36 | export const SCHEDULE = "Schedule"; 37 | export const PROFILE = "Profile"; 38 | export const BUDGET = "Budget"; 39 | export const TYPES = "Types"; 40 | 41 | export const MENU_ITEMS = [ 42 | { label: SCHEDULE, active: true }, 43 | { label: PROFILE, active: false, disabled: true }, 44 | { label: BUDGET, active: false, disabled: true }, 45 | { label: TYPES, active: false, disabled: true }, 46 | ]; 47 | 48 | export const SCHEDULE_FREQUENCY = [ 49 | { key: "0", value: "Daily", description: "" }, 50 | { key: "1", value: "Weekly", description: "" }, 51 | { key: "2", value: "Monthly", description: "" }, 52 | { key: "3", value: "Yearly", description: "" }, 53 | ]; 54 | -------------------------------------------------------------------------------- /src/components/ChartLegend/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | `; 6 | 7 | export const ChartContainer = styled.div` 8 | display: flex; 9 | height: 50px; 10 | width: 50px; 11 | `; 12 | 13 | export const Content = styled.div` 14 | display: flex; 15 | flex: 1; 16 | padding-left: 10px; 17 | border-bottom: 1px solid #c4c4c4; 18 | `; 19 | 20 | export const TitleContainer = styled.div` 21 | flex: 1; 22 | justify-content: center; 23 | display: flex; 24 | flex-direction: column; 25 | `; 26 | 27 | export const Title = styled.div` 28 | font-family: Roboto; 29 | font-style: normal; 30 | font-weight: normal; 31 | font-size: 12px; 32 | `; 33 | 34 | export const SubTitle = styled.div` 35 | font-family: Roboto; 36 | font-style: normal; 37 | font-weight: 300; 38 | font-size: 10px; 39 | `; 40 | 41 | export const DataContainer = styled.div` 42 | justify-content: center; 43 | display: flex; 44 | flex-direction: column; 45 | font-family: Roboto; 46 | text-align: right; 47 | span:first-child { 48 | font-style: normal; 49 | font-weight: normal; 50 | font-size: 12px; 51 | } 52 | span:last-child { 53 | font-style: normal; 54 | font-weight: 300; 55 | font-size: 10px; 56 | } 57 | `; 58 | -------------------------------------------------------------------------------- /src/components/Dropdown/styles.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import Select from "react-select"; 4 | 5 | const SelectComponent = (props) => ( 6 | ; 189 | }; 190 | 191 | const CreatableDropdownBox = styled(CreatableSelectComponent)` 192 | border: none; 193 | border-style: unset; 194 | z-index: 500; 195 | & .Select__control { 196 | border: none; 197 | border-style: unset; 198 | } 199 | 200 | & .Select__value-container { 201 | text-align: right; 202 | justify-content: flex-end; 203 | } 204 | 205 | & .Select__indicator-separator, 206 | .Select__clear-indicator { 207 | display: none; 208 | } 209 | 210 | & .Select__control--is-focused { 211 | box-shadow: none; 212 | } 213 | 214 | & .Select__option { 215 | text-align: right; 216 | } 217 | 218 | font-family: Roboto; 219 | border: none; 220 | border-radius: 7px; 221 | width: 100%; 222 | font-weight: normal; 223 | font-size: 13px; 224 | ::placeholder { 225 | color: #7e7e7e; 226 | } 227 | &:focus { 228 | outline: none; 229 | ::placeholder { 230 | color: #000; 231 | } 232 | } 233 | `; 234 | 235 | const DropdownBox = styled(SelectComponent)` 236 | border: none; 237 | border-style: unset; 238 | z-index: 400; 239 | & .Select__control { 240 | border: none; 241 | border-style: unset; 242 | } 243 | 244 | & .Select__value-container { 245 | text-align: right; 246 | justify-content: flex-end; 247 | } 248 | 249 | & .Select__indicator-separator, 250 | .Select__clear-indicator { 251 | display: none; 252 | } 253 | 254 | & .Select__control--is-focused { 255 | box-shadow: none; 256 | } 257 | 258 | & .Select__option { 259 | text-align: right; 260 | } 261 | 262 | font-family: Roboto; 263 | border: none; 264 | border-radius: 7px; 265 | width: 100%; 266 | font-weight: normal; 267 | font-size: 13px; 268 | ::placeholder { 269 | color: #7e7e7e; 270 | } 271 | &:focus { 272 | outline: none; 273 | ::placeholder { 274 | color: #000; 275 | } 276 | } 277 | `; 278 | 279 | const BigTextBox = styled.textarea` 280 | overflow: hidden; 281 | font-family: Roboto; 282 | resize: vertical; 283 | border: none; 284 | border: 1px solid #dedede; 285 | border-radius: 7px; 286 | width: 100%; 287 | padding: 10px; 288 | font-weight: normal; 289 | font-size: 13px; 290 | margin-bottom: 20px; 291 | ::placeholder { 292 | color: #7e7e7e; 293 | } 294 | &:focus { 295 | outline: none; 296 | border-bottom: 2px solid #38b44e; 297 | margin-bottom: 15px; 298 | ::placeholder { 299 | color: #000; 300 | } 301 | } 302 | `; 303 | 304 | const TextAreaBox = styled(BigTextBox)` 305 | min-height: 88px; 306 | `; 307 | 308 | export { 309 | InputContainer, 310 | InputBox, 311 | DropdownBox, 312 | CreatableDropdownBox, 313 | BigTextBox, 314 | TextAreaBox, 315 | TextMoney, 316 | Date, 317 | }; 318 | -------------------------------------------------------------------------------- /src/screens/Main/index.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState, useCallback } from "react"; 2 | import { GlobalContext, DispatchTypes } from "context"; 3 | import { LoadingComponent, ButtonComponent, InputComponent } from "components"; 4 | import { HeaderLayout } from "layouts"; 5 | import { getTypes, addRow, createDoc, HeaderData } from "services"; 6 | import Utils from "lib/utils"; 7 | import { useHistory } from "react-router-dom"; 8 | import { isMath, evaluateMath, isNumber } from "utils/math"; 9 | 10 | const Main = () => { 11 | const history = useHistory(); 12 | 13 | const { todayDate } = Utils.Date; 14 | 15 | const [mainLoading, setMainLoading] = useState(true); 16 | const [billsTypes, setBillsTypes] = useState([]); 17 | const [checked, setChecked] = useState(false); 18 | const [headerData, setHeaderData] = useState(null); 19 | 20 | const defaultForm = { 21 | amount: "", 22 | type: "", 23 | date: todayDate(), 24 | description: "", 25 | }; 26 | const [form, setForm] = useState(defaultForm); 27 | 28 | const clearForm = () => setForm(defaultForm); 29 | 30 | const addBillDisabled = !(form.amount && form.type && form.date); 31 | 32 | const onChange = (name, value) => { 33 | setForm({ 34 | ...form, 35 | [name]: value, 36 | }); 37 | }; 38 | 39 | const context = useContext(GlobalContext); 40 | const [userState, userDispatch] = context.globalUser; 41 | const [, modalDispatch] = context.globalModal; 42 | const { user, doc, loading } = userState; 43 | 44 | const alertModal = useCallback( 45 | (title, content, actions) => { 46 | modalDispatch({ 47 | type: DispatchTypes.Modal.MODAL_SHOW, 48 | title, 49 | content, 50 | actions: [ 51 | { 52 | text: "Ok", 53 | action: () => { 54 | modalDispatch({ type: DispatchTypes.Modal.MODAL_HIDE }); 55 | actions && actions(); 56 | }, 57 | }, 58 | ], 59 | }); 60 | }, 61 | [modalDispatch] 62 | ); 63 | 64 | const getStartData = useCallback( 65 | async (doc) => { 66 | setMainLoading(true); 67 | try { 68 | const types = await getTypes(doc); 69 | 70 | const typesFormatted = types.map((type) => ({ 71 | value: type, 72 | label: type, 73 | })); 74 | 75 | const headerData = await HeaderData(doc); 76 | 77 | setBillsTypes(typesFormatted); 78 | setHeaderData(headerData); 79 | 80 | setMainLoading(false); 81 | } catch (e) { 82 | console.log("getStartData ERROR", e); 83 | alertModal("Error", "There was an error. Please try again later."); 84 | setMainLoading(false); 85 | } 86 | }, 87 | [alertModal] 88 | ); 89 | 90 | const createDocHandler = useCallback(async () => { 91 | try { 92 | const { access_token, refresh_token, expires_at, spreadsheetId } = user; 93 | const normalizedId = Utils.Common.getSpreadsheetId(spreadsheetId); 94 | 95 | const newDoc = await createDoc( 96 | access_token, 97 | refresh_token, 98 | expires_at, 99 | normalizedId 100 | ); 101 | if (newDoc) { 102 | userDispatch({ 103 | type: DispatchTypes.User.GET_DOC_SUCCESS, 104 | doc: newDoc, 105 | }); 106 | } 107 | } catch (e) { 108 | setMainLoading(false); 109 | history.push("/"); 110 | console.log("createDoc ERROR", e); 111 | } 112 | }, [history, user, userDispatch]); 113 | 114 | useEffect(() => { 115 | if (doc === null && user && !checked) { 116 | setChecked(true); 117 | createDocHandler(); 118 | } 119 | }, [doc, user, checked, createDocHandler]); 120 | 121 | useEffect(() => { 122 | if (!loading && doc) { 123 | getStartData(doc); 124 | } 125 | }, [doc, loading, getStartData]); 126 | 127 | const addBill = async () => { 128 | const { type, date, description } = form; 129 | let { amount } = form; 130 | 131 | const isAmountMath = isMath(amount); 132 | const isAmountNumber = isNumber(amount); 133 | 134 | if( isAmountMath ){ 135 | try { 136 | amount = Math.round( evaluateMath(amount) ); 137 | } catch (error){ 138 | amount = ""; 139 | } 140 | } 141 | 142 | if( amount.length === 0 || !((isAmountNumber && !isAmountMath) || (!isAmountNumber && isAmountMath))){ 143 | alertModal( 144 | "Error", 145 | "There was an error trying to add a new bill. The amount field is not valid." 146 | ); 147 | setMainLoading(false); 148 | return; 149 | } 150 | 151 | if (window.gtag) { 152 | window.gtag("event", "add_bill", { 153 | ...form, 154 | }); 155 | } 156 | 157 | if (amount && type && date && user.name) { 158 | setMainLoading(true); 159 | try { 160 | const addAction = await addRow( 161 | doc, 162 | date, 163 | user.name, 164 | amount, 165 | type, 166 | description 167 | ); 168 | if (addAction) { 169 | const modalActionOk = () => getStartData(doc); 170 | alertModal( 171 | "Bill Added", 172 | "Your bill was successfully added!", 173 | modalActionOk 174 | ); 175 | clearForm(); 176 | } else { 177 | console.log("addBill ERROR", addAction); 178 | alertModal( 179 | "Error", 180 | "There was an error trying to add a new bill. Please try again later." 181 | ); 182 | setMainLoading(false); 183 | } 184 | setMainLoading(false); 185 | } catch (error) { 186 | console.log("addBill ERROR", error); 187 | alertModal( 188 | "Error", 189 | "There was an error trying to add a new bill. Please try again later." 190 | ); 191 | setMainLoading(false); 192 | } 193 | } else { 194 | console.log("addBill ERROR no user", user); 195 | alertModal( 196 | "Error", 197 | "There was an error trying to add a new bill. Please try again later." 198 | ); 199 | } 200 | }; 201 | 202 | const screenLoading = mainLoading || loading; 203 | 204 | return ( 205 | <> 206 | 207 | 214 | 223 | 230 | 238 |
239 | 244 |
245 |
246 | 247 | {screenLoading && } 248 | 249 | ); 250 | }; 251 | 252 | export default Main; 253 | -------------------------------------------------------------------------------- /src/screens/Type/index.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState, useCallback } from "react"; 2 | import { NoHeaderLayout } from "layouts"; 3 | import { 4 | PieChartComponent, 5 | ChartLegendComponent, 6 | DropdownComponent, 7 | LoadingComponent, 8 | BigModalComponent, 9 | DetailItemComponent, 10 | ButtonComponent, 11 | } from "components"; 12 | import * as S from "./styles"; 13 | import { GlobalContext, DispatchTypes } from "context"; 14 | import { 15 | getByTypesMonth, 16 | getMonthYears, 17 | getDetailsByTypeDate, 18 | getDetailsByMonth, 19 | deleteRow, 20 | } from "services"; 21 | import Utils from "lib/utils"; 22 | import { useLocation } from "react-router-dom"; 23 | 24 | const Type = () => { 25 | const location = useLocation(); 26 | 27 | const { nowYear, nowMonth, dateToText, monthToText } = Utils.Date; 28 | 29 | const locationStateMonth = parseInt(location?.state?.defaultMonth) - 1; 30 | 31 | const defaultMonth = isNaN(locationStateMonth) 32 | ? parseInt(nowMonth()) - 1 33 | : locationStateMonth; 34 | const defaultYear = location?.state?.defaultYear || nowYear().toString(); 35 | 36 | const { addColors } = Utils.Colors; 37 | const { MONTHS } = Utils.Constants; 38 | 39 | const [mainLoading, setMainLoading] = useState(true); 40 | const [selectedDate, setSelectedDate] = useState({ 41 | month: defaultMonth, 42 | year: defaultYear, 43 | }); 44 | 45 | const [monthsOptions, setMonthsOptions] = useState([]); 46 | const [data, setData] = useState([]); 47 | const context = useContext(GlobalContext); 48 | const [userState] = context.globalUser; 49 | const [, modalDispatch] = context.globalModal; 50 | const { doc, loading } = userState; 51 | const [showDetail, setShowDetail] = useState(""); 52 | const [detailData, setDetailData] = useState([]); 53 | const [modalTitle, setModalTitle] = useState(""); 54 | const [modalSubtitle, setModalSubtitle] = useState(""); 55 | 56 | const getStartData = useCallback( 57 | async (doc) => { 58 | setMainLoading(true); 59 | const selectedMonth = parseInt(selectedDate.month) + 1; 60 | const typesMonth = await getByTypesMonth( 61 | doc, 62 | selectedMonth, 63 | selectedDate.year 64 | ); 65 | const typesMonthWithColors = addColors(typesMonth); 66 | const monthYears = await getMonthYears(doc); 67 | 68 | const grouppedYears = [...new Set(monthYears.map(({ year }) => year))]; 69 | const grouppedMonths = grouppedYears.map((year) => ({ 70 | label: year, 71 | options: monthYears 72 | .filter((item) => item.year === year) 73 | .map((item) => ({ 74 | value: { month: parseInt(item.month) - 1, year: item.year }, 75 | label: `${MONTHS[parseInt(item.month) - 1]}`, 76 | })), 77 | })); 78 | 79 | setMonthsOptions(grouppedMonths); 80 | 81 | setData(typesMonthWithColors); 82 | setMainLoading(false); 83 | }, 84 | [MONTHS, addColors, selectedDate.month, selectedDate.year] 85 | ); 86 | 87 | const getDetailData = async (type) => { 88 | setMainLoading(true); 89 | setModalTitle(type); 90 | const selectedMonth = parseInt(selectedDate.month) + 1; 91 | const data = await getDetailsByTypeDate( 92 | doc, 93 | selectedMonth, 94 | selectedDate.year, 95 | type 96 | ); 97 | setDetailData(data); 98 | setMainLoading(false); 99 | setShowDetail("single"); 100 | const subTitle = `${data.length} transactions`; 101 | setModalSubtitle(subTitle); 102 | }; 103 | 104 | const total = data.reduce((prev, cur) => prev + cur.value, 0); 105 | 106 | const onChangeDate = (value) => { 107 | if ( 108 | `${value.month}-${value.year}` !== 109 | `${selectedDate.month}-${selectedDate.year}` 110 | ) { 111 | const newValue = { ...value }; 112 | setSelectedDate(newValue); 113 | } 114 | }; 115 | 116 | const screenLoading = mainLoading || loading; 117 | 118 | useEffect(() => { 119 | if (!loading && doc) { 120 | getStartData(doc); 121 | } 122 | }, [getStartData, doc, loading, selectedDate]); 123 | 124 | const flattedOptions = monthsOptions 125 | .map(({ options }) => options) 126 | .flat() 127 | .find( 128 | ({ value }) => 129 | parseInt(value.year) === parseInt(selectedDate.year) && 130 | parseInt(value.month) === parseInt(selectedDate.month) 131 | ); 132 | 133 | const handleCloseDetail = () => { 134 | setShowDetail(""); 135 | setDetailData([]); 136 | setModalTitle(""); 137 | setModalSubtitle(""); 138 | }; 139 | 140 | const showAllDetails = async () => { 141 | setMainLoading(true); 142 | 143 | const title = `${monthToText(parseInt(selectedDate.month))} ${ 144 | selectedDate.year 145 | }`; 146 | setModalTitle(title); 147 | 148 | const selectedMonth = parseInt(selectedDate.month) + 1; 149 | const data = await getDetailsByMonth(doc, selectedMonth, selectedDate.year); 150 | setShowDetail("all"); 151 | setDetailData(data); 152 | const subTitle = `${data.length} transactions`; 153 | setModalSubtitle(subTitle); 154 | setMainLoading(false); 155 | }; 156 | 157 | const deleteRecord = async (id, type) => { 158 | modalDispatch({ 159 | type: DispatchTypes.Modal.MODAL_SHOW, 160 | title: "Confirmation", 161 | content: "Do you really want to delete this record?", 162 | actions: [ 163 | { 164 | type: "secondary", 165 | text: "Delete", 166 | action: async () => { 167 | modalDispatch({ type: DispatchTypes.Modal.MODAL_HIDE }); 168 | setMainLoading(true); 169 | await deleteRow(doc, id); 170 | getStartData(doc); 171 | if (showDetail === "all") { 172 | showAllDetails(); 173 | } else { 174 | getDetailData(type); 175 | } 176 | setMainLoading(false); 177 | }, 178 | }, 179 | { 180 | type: "text", 181 | text: "Cancel", 182 | action: () => { 183 | modalDispatch({ type: DispatchTypes.Modal.MODAL_HIDE }); 184 | }, 185 | }, 186 | ], 187 | }); 188 | }; 189 | 190 | return ( 191 | <> 192 | 193 | 194 | 195 | Expenses 196 | { 200 | onChangeDate(value); 201 | }} 202 | placeholder="Select" 203 | value={flattedOptions} 204 | /> 205 | 206 | 213 |
214 | {data.map((item, index) => ( 215 | getDetailData(item.name)} 223 | /> 224 | ))} 225 | 226 | 231 | 232 |
233 |
234 |
235 | {showDetail && ( 236 | handleCloseDetail()} 240 | > 241 | {detailData.map(({ Amount, Date, Detail, Type, Id }, index) => ( 242 | deleteRecord(Id, Type)} 249 | /> 250 | ))} 251 | 252 | )} 253 | {screenLoading && } 254 | 255 | ); 256 | }; 257 | 258 | export default Type; 259 | -------------------------------------------------------------------------------- /src/screens/Analytics/index.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState, useCallback } from "react"; 2 | import { NoHeaderLayout } from "layouts"; 3 | import { 4 | DropdownComponent, 5 | LoadingComponent, 6 | BarChartComponent, 7 | MonthLegendComponent, 8 | LineChartComponent, 9 | CheckboxComponent, 10 | } from "components"; 11 | import * as S from "./styles"; 12 | import { GlobalContext } from "context"; 13 | import { 14 | getYears, 15 | getAllMonthByYear, 16 | getLast12Months, 17 | getLast12MonthsByType, 18 | getAllMonthByTypeByYear, 19 | } from "services"; 20 | import Utils from "lib/utils"; 21 | import { useHistory } from "react-router-dom"; 22 | 23 | const Analytics = () => { 24 | const history = useHistory(); 25 | const context = useContext(GlobalContext); 26 | const [userState] = context.globalUser; 27 | const { doc, loading } = userState; 28 | 29 | const { formatSymbol } = Utils.Currency; 30 | const { LAST_12_MONTHS_OPTION } = Utils.Constants; 31 | const { nextMonth } = Utils.Date; 32 | const { addColors } = Utils.Colors; 33 | 34 | const [mainLoading, setMainLoading] = useState(false); 35 | const [selectedYear, setSelectedYear] = useState(LAST_12_MONTHS_OPTION.value); 36 | const [yearsOption, setYearsOption] = useState([]); 37 | const [data, setData] = useState([]); 38 | const [legendData, setLegendData] = useState([]); 39 | const [chartCategories, setChartCategories] = useState([]); 40 | const [showTypes, setShowTypes] = useState(false); 41 | const [totalYear, setTotalYear] = useState(0); 42 | 43 | const getStartData = useCallback( 44 | async (doc) => { 45 | setMainLoading(true); 46 | const years = await getYears(doc); 47 | const newYearsOptions = years.map((y) => ({ label: y, value: y })); 48 | newYearsOptions.push(LAST_12_MONTHS_OPTION); 49 | setYearsOption(newYearsOptions); 50 | 51 | const getChartDataTypes = () => { 52 | if (selectedYear === "last12months") { 53 | return getLast12MonthsByType(doc); 54 | } else { 55 | return getAllMonthByTypeByYear(doc, selectedYear); 56 | } 57 | }; 58 | 59 | const getChartData = () => { 60 | if (selectedYear === "last12months") { 61 | return getLast12Months(doc); 62 | } else { 63 | return getAllMonthByYear(doc, selectedYear); 64 | } 65 | }; 66 | 67 | const newChartData = showTypes 68 | ? await getChartDataTypes() 69 | : await getChartData(); 70 | 71 | const arrayStartsFrom = selectedYear === "last12months" ? nextMonth() : 1; 72 | 73 | const newArray = 74 | selectedYear === "last12months" 75 | ? newChartData.map(({ name }) => parseInt(name)) 76 | : Array.from({ length: 12 }, (_, i) => 77 | i + arrayStartsFrom > 12 78 | ? (i + arrayStartsFrom + 1) % 13 79 | : i + arrayStartsFrom 80 | ); 81 | 82 | const chartDataWithAllMonths = newArray.map((index) => { 83 | const findElement = newChartData.find( 84 | ({ name }) => parseInt(name) === index 85 | ); 86 | 87 | const newName = index < 10 ? `0${index}` : `${index}`; 88 | 89 | if (showTypes) { 90 | if (findElement) { 91 | const keys = Object.keys(findElement); 92 | const newKeys = keys.filter((e) => e !== "name" && e !== "year"); 93 | const newValues = newKeys 94 | .map((e) => ({ 95 | [e]: findElement[e].value, 96 | })) 97 | .reduce( 98 | (prev, curr) => ({ 99 | ...prev, 100 | ...curr, 101 | }), 102 | {} 103 | ); 104 | const newObject = { 105 | ...newValues, 106 | name: newName, 107 | year: findElement[newKeys[0]]?.year, 108 | }; 109 | return newObject; 110 | } else { 111 | return { name: newName }; 112 | } 113 | } else { 114 | return findElement 115 | ? findElement 116 | : { value: 0, count: 0, year: 0, name: newName }; 117 | } 118 | }); 119 | 120 | const yearData = await getChartData(); 121 | const leggendsData = newArray 122 | .map((index) => { 123 | const findElement = yearData.find( 124 | ({ name }) => parseInt(name) === index 125 | ); 126 | const newName = index < 10 ? `0${index}` : `${index}`; 127 | return findElement 128 | ? findElement 129 | : { value: 0, count: 0, year: 0, name: newName }; 130 | }) 131 | .filter(({ value }) => value > 0) 132 | .sort((a, b) => 133 | parseInt(`${b.year}${b.name}`) > parseInt(`${a.year}${a.name}`) 134 | ? 1 135 | : -1 136 | ); 137 | const newTotalYear = newArray 138 | .map((index) => { 139 | const findElement = yearData.find( 140 | ({ name }) => parseInt(name) === index 141 | ); 142 | const newName = index < 10 ? `0${index}` : `${index}`; 143 | return findElement 144 | ? findElement 145 | : { value: 0, count: 0, year: 0, name: newName }; 146 | }) 147 | .filter(({ value }) => value > 0) 148 | .reduce((acc, cur) => acc + cur.value, 0); 149 | 150 | setTotalYear(newTotalYear); 151 | setLegendData(leggendsData); 152 | setData(chartDataWithAllMonths); 153 | setMainLoading(false); 154 | }, 155 | [LAST_12_MONTHS_OPTION, nextMonth, selectedYear, showTypes] 156 | ); 157 | 158 | useEffect(() => { 159 | const categories = showTypes 160 | ? addColors( 161 | [...new Set(data.map((e) => Object.keys(e)).flat())] 162 | .filter((e) => e !== "name" && e !== "year") 163 | .map((e, index) => ({ 164 | key: e, 165 | checked: index <= 2, 166 | })) 167 | ) 168 | : []; 169 | if (categories) { 170 | setChartCategories(categories); 171 | } 172 | }, [addColors, data, showTypes]); 173 | 174 | useEffect(() => { 175 | if (doc) { 176 | getStartData(doc); 177 | } 178 | }, [doc, getStartData, selectedYear]); 179 | 180 | const onChangeYear = (newYear) => { 181 | setSelectedYear(newYear); 182 | }; 183 | 184 | const yearSelectedValueOption = yearsOption.find( 185 | ({ value }) => value === selectedYear.toString() 186 | ); 187 | 188 | const chartData = showTypes 189 | ? data 190 | : data.map(({ value, name }) => ({ 191 | name, 192 | amount: value, 193 | })); 194 | 195 | const handleCategoryChange = (key) => { 196 | const newCategories = [...chartCategories].map((category) => ({ 197 | ...category, 198 | checked: category.key === key ? !category.checked : category.checked, 199 | })); 200 | setChartCategories(newCategories); 201 | }; 202 | 203 | const screenLoading = mainLoading || loading; 204 | 205 | return ( 206 | <> 207 | 208 | 209 | 210 | Expenses 211 | { 215 | onChangeYear(value); 216 | }} 217 | placeholder="Select" 218 | value={yearSelectedValueOption} 219 | /> 220 | 221 | 222 | {`Summary of ${ 223 | selectedYear === "last12months" ? "last 12 months" : selectedYear 224 | }`} 225 | 226 | ${formatSymbol(totalYear)} this{" "} 227 | {selectedYear === "last12months" ? "period" : "year"} 228 | 229 | 230 | <> 231 | setShowTypes(!showTypes)} 235 | label="By type" 236 | /> 237 | 238 | {showTypes ? ( 239 | <> 240 | checked)} 244 | /> 245 | 246 | {chartCategories.map(({ key, checked, color }, index) => ( 247 |
  • 248 | handleCategoryChange(key)} 252 | color={color} 253 | /> 254 |
  • 255 | ))} 256 |
    257 | 258 | ) : ( 259 | 260 | )} 261 | 262 | {legendData.map(({ name, value, year }, index) => ( 263 | { 269 | history.push({ 270 | pathname: "/types", 271 | state: { defaultMonth: name, defaultYear: year }, 272 | }); 273 | }} 274 | /> 275 | ))} 276 | 277 |
    278 |
    279 | 280 | {screenLoading && } 281 | 282 | ); 283 | }; 284 | 285 | export default Analytics; 286 | --------------------------------------------------------------------------------