import React, { useMemo } from 'react'; import { DndProvider } from 'react-dnd'; import Backend from 'react-dnd-html5-backend'; import { connect } from 'react-redux'; import { Router, Route, Redirect, Switch, useLocation, NavLink } from 'react-router-dom'; import { CompatRouter } from 'react-router-dom-v5-compat'; import { createBrowserHistory } from 'history'; import hotkeys from 'hotkeys-js'; import * as actions from 'loot-core/src/client/actions'; import { AccountsProvider } from 'loot-core/src/client/data-hooks/accounts'; import { PayeesProvider } from 'loot-core/src/client/data-hooks/payees'; import { SpreadsheetProvider } from 'loot-core/src/client/SpreadsheetProvider'; import checkForUpgradeNotifications from 'loot-core/src/client/upgrade-notifications'; import * as undo from 'loot-core/src/platform/client/undo'; import { BudgetMonthCountProvider } from 'loot-design/src/components/budget/BudgetMonthCountContext'; import { View } from 'loot-design/src/components/common'; import { colors, styles } from 'loot-design/src/style'; import Cog from 'loot-design/src/svg/v1/Cog'; import PiggyBank from 'loot-design/src/svg/v1/PiggyBank'; import Wallet from 'loot-design/src/svg/v1/Wallet'; import { isMobile } from '../util'; import { getLocationState, makeLocationState } from '../util/location-state'; import Account from './accounts/Account'; import { default as MobileAccount } from './accounts/MobileAccount'; import { default as MobileAccounts } from './accounts/MobileAccounts'; import { ActiveLocationProvider } from './ActiveLocation'; import BankSyncStatus from './BankSyncStatus'; import Budget from './budget'; import { default as MobileBudget } from './budget/MobileBudget'; import FloatableSidebar, { SidebarProvider } from './FloatableSidebar'; import GlobalKeys from './GlobalKeys'; import { ManageRulesPage } from './ManageRulesPage'; import Modals from './Modals'; import Notifications from './Notifications'; import { PageTypeProvider } from './Page'; import { ManagePayeesPage } from './payees/ManagePayeesPage'; import Reports from './reports'; import Schedules from './schedules'; import DiscoverSchedules from './schedules/DiscoverSchedules'; import EditSchedule from './schedules/EditSchedule'; import LinkSchedule from './schedules/LinkSchedule'; import PostsOfflineNotification from './schedules/PostsOfflineNotification'; import Settings from './settings'; import Titlebar, { TitlebarProvider } from './Titlebar'; import FixSplitsTool from './tools/FixSplitsTool'; // import Debugger from './Debugger'; function PageRoute({ path, component: Component }) { return ( <Route path={path} children={props => { return ( <View style={{ flex: 1, display: props.match ? 'flex' : 'none' }} > <Component {...props} /> </View> ); }} /> ); } function Routes({ isMobile, location }) { return ( <Switch location={location}> <Route path="/"> <Route path="/" exact render={() => <Redirect to="/budget" />} /> <PageRoute path="/reports" component={Reports} /> <PageRoute path="/budget" component={isMobile ? MobileBudget : Budget} /> <Route path="/schedules" exact component={Schedules} /> <Route path="/schedule/edit" exact component={EditSchedule} /> <Route path="/schedule/edit/:id" component={EditSchedule} /> <Route path="/schedule/link" component={LinkSchedule} /> <Route path="/schedule/discover" component={DiscoverSchedules} /> <Route path="/schedule/posts-offline-notification" component={PostsOfflineNotification} /> <Route path="/rules" exact component={ManageRulesPage} /> <Route path="/payees" exact component={ManagePayeesPage} /> <Route path="/tools/fix-splits" exact component={FixSplitsTool} /> <Route path="/accounts/:id" exact children={props => { const AcctCmp = isMobile ? MobileAccount : Account; return ( props.match && <AcctCmp key={props.match.params.id} {...props} /> ); }} /> <Route path="/accounts" exact component={isMobile ? MobileAccounts : Account} /> <Route path="/settings" component={Settings} /> </Route> </Switch> ); } function StackedRoutes({ isMobile }) { let location = useLocation(); let locationPtr = getLocationState(location, 'locationPtr'); let locations = [location]; while (locationPtr) { locations.unshift(locationPtr); locationPtr = getLocationState(locationPtr, 'locationPtr'); } let base = locations[0]; let stack = locations.slice(1); return ( <ActiveLocationProvider location={locations[locations.length - 1]}> <Routes location={base} isMobile={isMobile} /> {stack.map((location, idx) => ( <PageTypeProvider key={location.key} type="modal" current={idx === stack.length - 1} > <Routes location={location} isMobile={isMobile} /> </PageTypeProvider> ))} </ActiveLocationProvider> ); } function NavTab({ icon: TabIcon, name, path }) { return ( <NavLink to={path} style={{ alignItems: 'center', color: '#8E8E8F', display: 'flex', flexDirection: 'column', textDecoration: 'none' }} activeStyle={{ color: colors.p5 }} > <TabIcon width={22} height={22} style={{ color: 'inherit', marginBottom: '5px' }} /> {name} </NavLink> ); } function MobileNavTabs() { return ( <div style={{ backgroundColor: 'white', borderTop: `1px solid ${colors.n10}`, bottom: 0, ...styles.shadow, display: 'flex', height: '80px', justifyContent: 'space-around', paddingTop: 10, width: '100%' }} > <NavTab name="Budget" path="/budget" icon={Wallet} isActive={false} /> <NavTab name="Accounts" path="/accounts" icon={PiggyBank} isActive={false} /> <NavTab name="Settings" path="/settings" icon={Cog} isActive={false} /> </div> ); } class FinancesApp extends React.Component { constructor(props) { super(props); this.state = { isMobile: isMobile(window.innerWidth) }; this.history = createBrowserHistory(); let oldPush = this.history.push; this.history.push = (to, state) => { return oldPush.call(this.history, to, makeLocationState(state)); }; // I'm not sure if this is the best approach but we need this to // globally. We could instead move various workflows inside global // React components, but that's for another day. window.__history = this.history; undo.setUndoState('url', window.location.href); this.cleanup = this.history.listen(location => { undo.setUndoState('url', window.location.href); }); this.handleWindowResize = this.handleWindowResize.bind(this); } handleWindowResize() { this.setState({ isMobile: isMobile(window.innerWidth), windowWidth: window.innerWidth }); } componentDidMount() { // TODO: quick hack fix for showing the demo if (this.history.location.pathname === '/subscribe') { this.history.push('/'); } // Get the accounts and check if any exist. If there are no // accounts, we want to redirect the user to the All Accounts // screen which will prompt them to add an account this.props.getAccounts().then(accounts => { if (accounts.length === 0) { this.history.push('/accounts'); } }); // The default key handler scope hotkeys.setScope('app'); // Wait a little bit to make sure the sync button will get the // sync start event. This can be improved later. setTimeout(async () => { await this.props.sync(); // Check for upgrade notifications. We do this after syncing // because these states are synced across devices, so they will // only see it once for this file checkForUpgradeNotifications( this.props.addNotification, this.props.resetSync, this.history ); }, 100); window.addEventListener('resize', this.handleWindowResize); } componentWillUnmount() { this.cleanup(); window.removeEventListener('resize', this.handleWindowResize); } render() { return ( <Router history={this.history}> <CompatRouter> <View style={{ height: '100%', backgroundColor: colors.n10 }}> <GlobalKeys /> <View style={{ flexDirection: 'row', flex: 1 }}> {!this.state.isMobile && <FloatableSidebar />} <div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', position: 'relative', width: '100%' }} > {!this.state.isMobile && ( <Titlebar style={{ WebkitAppRegion: 'drag', position: 'absolute', top: 0, left: 0, right: 0, zIndex: 1000 }} /> )} <div style={{ flex: 1, display: 'flex', overflow: 'auto', position: 'relative' }} > <Notifications /> <BankSyncStatus /> <StackedRoutes isMobile={this.state.isMobile} /> {/*window.Actual.IS_DEV && <Debugger />*/} <Modals history={this.history} /> </div> {this.state.isMobile && ( <Switch> <Route path="/budget" component={MobileNavTabs} /> <Route path="/accounts" component={MobileNavTabs} /> <Route path="/settings" component={MobileNavTabs} /> </Switch> )} </div> </View> </View> </CompatRouter> </Router> ); } } function FinancesAppWithContext(props) { let app = useMemo(() => <FinancesApp {...props} />, [props]); return ( <SpreadsheetProvider> <TitlebarProvider> <SidebarProvider> <BudgetMonthCountProvider> <PayeesProvider> <AccountsProvider> <DndProvider backend={Backend}>{app}</DndProvider> </AccountsProvider> </PayeesProvider> </BudgetMonthCountProvider> </SidebarProvider> </TitlebarProvider> </SpreadsheetProvider> ); } export default connect(null, actions)(FinancesAppWithContext);