diff --git a/package-lock.json b/package-lock.json index 36466f6..81d08a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2723,6 +2723,11 @@ "@types/gapi": "*" } }, + "@types/history": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.2.tgz", + "integrity": "sha512-ui3WwXmjTaY73fOQ3/m3nnajU/Orhi6cEu5rzX+BrAAJxa3eITXZ5ch9suPqtM03OWhAHhPSyBGCN4UKoxO20Q==" + }, "@types/istanbul-lib-coverage": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.0.tgz", @@ -2775,6 +2780,25 @@ "@types/react": "*" } }, + "@types/react-router": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-4.4.5.tgz", + "integrity": "sha512-12+VOu1+xiC8RPc9yrgHCyLI79VswjtuqeS2gPrMcywH6tkc8rGIUhs4LaL3AJPqo5d+RPnfRpNKiJ7MK2Qhcg==", + "requires": { + "@types/history": "*", + "@types/react": "*" + } + }, + "@types/react-router-dom": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-4.3.2.tgz", + "integrity": "sha512-biesHodFxPgDxku2m08XwPeAfUYBcxAnrQG7pwFikuA3L2e3u2OKAb+Sb16bJuU3L5CTHd+Ivap+ke4mmGsHqQ==", + "requires": { + "@types/history": "*", + "@types/react": "*", + "@types/react-router": "*" + } + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", diff --git a/package.json b/package.json index 9c679a4..7e0ad2d 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@types/node": "^11.13.4", "@types/react": "^16.8.13", "@types/react-dom": "^16.8.4", + "@types/react-router-dom": "^4.3.2", "material-components-web": "^1.1.1", "material-icons": "^0.3.1", "node-sass": "^4.11.0", diff --git a/src/App.js b/src/App.js index 3ead7dc..7168428 100644 --- a/src/App.js +++ b/src/App.js @@ -1,14 +1,27 @@ import React, { Component } from "react"; -import { Snackbar } from "@material/react-snackbar"; -import { BrowserRouter, Route } from "react-router-dom"; - -import Settings from "./components/settings/settings"; -import Dashboard from "./components/dashboard/dashboard"; - import "./App.scss"; - -class App extends Component { +import Main from "./components/main"; +import { BrowserRouter } from "react-router-dom"; + +export default class App extends Component { + menu = [ + { + url: '/', + icon: "dashboard", + text: "Dashboard" + }, + { + url: '/charts', + icon: "pie_chart", + text: "Charts" + }, + { + url: '/settings', + icon: "settings", + text: "Settings" + } + ]; constructor(props) { super(props); @@ -156,22 +169,14 @@ class App extends Component { render() { return ( - } - /> - } - /> - - ); } } - -export default App; diff --git a/src/components/dashboard/dashboard.js b/src/components/dashboard/dashboard.js index 506ce30..3377b86 100644 --- a/src/components/dashboard/dashboard.js +++ b/src/components/dashboard/dashboard.js @@ -15,9 +15,6 @@ import '@material/react-fab/dist/fab.css'; import { withRouter } from "react-router-dom"; -import TopBar from "../top-bar/top-bar"; -import Hamburger from "../hamburger/hamburger"; - class Dashboard extends Component { clientId = process.env.REACT_APP_GOOGLE_CLIENT_ID || @@ -26,27 +23,10 @@ class Dashboard extends Component { process.env.REACT_APP_SHEET_ID || "1eYrQf0xhs2mTSWEzQRfSM-MD-tCcx1r0NVEacLg3Jrc"; - menu = [ - { - url: '/', - icon: "dashboard", - text: "Dashboard" - }, - { - url: '/charts', - icon: "pie_chart", - text: "Charts" - }, - { - url: '/settings', - icon: "settings", - text: "Settings" - } - ]; - constructor(props) { super(props); this.state = props.state; + this.state.expenses = []; } componentDidMount() { @@ -74,10 +54,10 @@ class Dashboard extends Component { signedInChanged = (signedIn) => { this.setState({ signedIn }); if (this.state.signedIn) { - this.setState({ profile: window.gapi.auth2.getAuthInstance().currentUser.get().getBasicProfile() }); + this.props.signedInChanged(window.gapi.auth2.getAuthInstance().currentUser.get().getBasicProfile()); this.load(); } else { - this.setState({ profile: null }); + this.props.signedInChanged(null); } }; @@ -125,34 +105,9 @@ class Dashboard extends Component { }; } - openDrawer = () => { - this.setState({ drawerOpen: true }); - }; - - closeDrawer = () => { - this.setState({ drawerOpen: false }); - }; - - navigateTo = (url) => { - this.props.history.push(url) - }; - render() { return ( -
- - + {this.state.signedIn === undefined && } {this.state.signedIn === false && @@ -169,7 +124,7 @@ class Dashboard extends Component {
} {this.state.signedIn && this.renderBody()} - + ); } diff --git a/src/components/hamburger/hamburger.tsx b/src/components/hamburger/hamburger.tsx index 3b50766..b3ce240 100644 --- a/src/components/hamburger/hamburger.tsx +++ b/src/components/hamburger/hamburger.tsx @@ -9,16 +9,16 @@ import "@material/react-drawer/dist/drawer.css"; import List, { ListItem, ListItemGraphic, ListItemText } from "@material/react-list"; import '@material/react-list/dist/list.css'; -type Props = { +type HamburgerProps = { drawerOpen?: boolean closeDrawer: () => void navigateTo: (url: string) => void - profile?: gapi.auth2.BasicProfile + profile?: gapi.auth2.BasicProfile | null selectedMenuIndex?: number menu: Menu } -type State = {} +type HamburgerState = {} type MenuItem = { url: string, @@ -26,9 +26,9 @@ type MenuItem = { text: string } -type Menu = MenuItem[] +export type Menu = MenuItem[] -export default class Hamburger extends Component { +export default class Hamburger extends Component { private closeDrawer = () => { if (typeof this.props.closeDrawer === 'function') { this.props.closeDrawer(); @@ -45,9 +45,11 @@ export default class Hamburger extends Component { } }; - render(): React.ReactElement> { + render(): React.ReactElement> { const menu = this.props.menu.map((menuItem: MenuItem) => - + } /> diff --git a/src/components/main.tsx b/src/components/main.tsx new file mode 100644 index 0000000..4b897b9 --- /dev/null +++ b/src/components/main.tsx @@ -0,0 +1,88 @@ +import React, { Component } from "react"; + +import { Route, RouteComponentProps, withRouter } from "react-router-dom"; +import TopBar from "./top-bar/top-bar"; +import Hamburger, { Menu } from "./hamburger/hamburger"; +import Dashboard from "./dashboard/dashboard"; +import Settings from "./settings/settings"; +import { Snackbar } from "@material/react-snackbar"; + +type MainProps = { + selectedMenuIndex: number, + openDrawer: () => void, + closeDrawer: () => void, + navigateTo: () => void, + menu: Menu, +} + +type MainState = { + snackbarMessage: string, + drawerOpen: boolean + profile: gapi.auth2.BasicProfile | null +} + +class Main extends Component & MainProps, MainState> { + state: MainState = { + snackbarMessage: '', + drawerOpen: false, + profile: null, + }; + + openDrawer = () => { + this.setState({ drawerOpen: true }); + }; + + closeDrawer = () => { + this.setState({ drawerOpen: false }); + }; + + navigateTo = (url: string) => { + this.props.history.push(url); + }; + + signedInChanged = (profile: gapi.auth2.BasicProfile | null) => { + this.setState({ profile }); + }; + + render(): React.ReactElement & MainProps, React.JSXElementConstructor> { + return ( + + + +
+ + } + /> + } + /> +
+ +
+ ); + } +} + +export default withRouter(Main); diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index 4cbeddc..f434377 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -1,12 +1,171 @@ import React, { Component } from "react"; +import { LoadingBar, OperationForm, OperationsList } from "../index"; + +import { TopAppBarFixedAdjust } from "@material/react-top-app-bar"; +import '@material/react-top-app-bar/dist/top-app-bar.css'; + +import MaterialIcon from "@material/react-material-icon"; +import '@material/react-material-icon/dist/material-icon.css'; + +import { Fab } from "@material/react-fab"; +import '@material/react-fab/dist/fab.css'; + +import { withRouter } from "react-router-dom"; + class Settings extends Component { + clientId = + process.env.REACT_APP_GOOGLE_CLIENT_ID || + "826265862385-p41e559ccssujlfsf49ppmo0gktkf6co.apps.googleusercontent.com"; + spreadsheetId = + process.env.REACT_APP_SHEET_ID || + "1eYrQf0xhs2mTSWEzQRfSM-MD-tCcx1r0NVEacLg3Jrc"; + + constructor(props) { + super(props); + this.state = props.state; + } + + componentDidMount() { + window.gapi.load("client:auth2", () => { + window.gapi.client + .init({ + discoveryDocs: [ + "https://sheets.googleapis.com/$discovery/rest?version=v4" + ], + clientId: this.clientId, + scope: + "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.metadata.readonly" + }) + .then(() => { + window.gapi.auth2 + .getAuthInstance() + .isSignedIn.listen(this.signedInChanged); + this.signedInChanged( + window.gapi.auth2.getAuthInstance().isSignedIn.get() + ); + }); + }); + } + + signedInChanged = (signedIn) => { + this.setState({ signedIn }); + if (this.state.signedIn) { + this.setState({ profile: window.gapi.auth2.getAuthInstance().currentUser.get().getBasicProfile() }); + this.load(); + } else { + this.setState({ profile: null }); + } + }; + + load() { + window.gapi.client.sheets.spreadsheets.values + .batchGet({ + spreadsheetId: this.spreadsheetId, + ranges: [ + "Data!A2:A50", + "Data!E2:E50", + "Expenses!A2:F", + "Current!H1", + "Previous!H1" + ] + }) + .then(response => { + const accounts = response.result.valueRanges[0].values.map( + items => items[0] + ); + const categories = response.result.valueRanges[1].values.map( + items => items[0] + ); + this.setState({ + accounts: accounts, + categories: categories, + expenses: (response.result.valueRanges[2].values || []) + .map(Settings.parseExpense) + .reverse() + .slice(0, 30), + processing: false, + currentMonth: response.result.valueRanges[3].values[0][0], + previousMonth: response.result.valueRanges[4].values[0][0] + }); + }); + } + + static parseExpense(value, index) { + return { + id: `Expenses!A${index + 2}`, + date: value[0], + description: value[1], + category: value[3], + amount: value[4].replace(",", ""), + account: value[2] + }; + } render() { return ( -

test

+ + +

test

+ {this.state.signedIn === undefined && } + {this.state.signedIn === false && +
+ +
} + {this.state.signedIn && this.renderBody()} +
+
); } + + renderBody() { + if (this.state.processing) return ; + else + return ( +
+ {this.renderExpenses()} +
+ ); + } + + renderExpenses() { + if (this.state.showExpenseForm) + return ( + + ); + else { // noinspection RequiredAttributes + return ( +
+ + this.onExpenseNew()} + className="add-transaction-fab--fixed" + aria-label="Add expense" + icon={} + /> +
+ ); + } + } } -export default Settings; +export default withRouter(Settings); diff --git a/src/components/top-bar/top-bar.tsx b/src/components/top-bar/top-bar.tsx index f13d53d..a475587 100644 --- a/src/components/top-bar/top-bar.tsx +++ b/src/components/top-bar/top-bar.tsx @@ -9,14 +9,15 @@ import TopAppBar, { } from "@material/react-top-app-bar"; import '@material/react-top-app-bar/dist/top-app-bar.css'; -type Props = { +type TopBarProps = { title?: string, - openDrawer: () => void + openDrawer: () => void, + short?: boolean } -type State = {} +type TopBarState = {} -export default class TopBar extends Component { +export default class TopBar extends Component { private openDrawer = () => { if (typeof this.props.openDrawer === 'function') { this.props.openDrawer(); @@ -25,10 +26,10 @@ export default class TopBar extends Component { } }; - render(): React.ReactElement> { + render(): React.ReactElement> { // noinspection HtmlDeprecatedAttribute return