├── src ├── App.css ├── components │ ├── layout │ │ ├── spinner.gif │ │ ├── Sidebar.js │ │ ├── Spinner.js │ │ ├── Dashboard.js │ │ ├── Alert.js │ │ └── AppNavbar.js │ ├── auth │ │ ├── Login.js │ │ └── Register.js │ ├── clients │ │ ├── Clients.js │ │ ├── AddClient.js │ │ ├── EditClient.js │ │ └── ClientDetails.js │ └── settings │ │ └── Settings.js ├── actions │ ├── notifyActions.js │ ├── types.js │ └── settingsActions.js ├── index.js ├── App.test.js ├── reducers │ ├── notifyReducer.js │ └── settingsReducer.js ├── helpers │ └── auth.js ├── store.js ├── App.js └── registerServiceWorker.js ├── public ├── favicon.ico ├── manifest.json └── index.html ├── .gitignore ├── README.md └── package.json /src/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/clientpanel_react/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/components/layout/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/clientpanel_react/HEAD/src/components/layout/spinner.gif -------------------------------------------------------------------------------- /src/actions/notifyActions.js: -------------------------------------------------------------------------------- 1 | import { NOTIFY_USER } from './types'; 2 | 3 | export const notifyUser = (message, messageType) => { 4 | return { 5 | type: NOTIFY_USER, 6 | message, 7 | messageType 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const NOTIFY_USER = 'NOTIFY_USER'; 2 | export const DISABLE_BALANCE_ON_ADD = 'DISABLE_BALANCE_ON_ADD'; 3 | export const DISABLE_BALANCE_ON_EDIT = 'DISABLE_BALANCE_ON_EDIT'; 4 | export const ALLOW_REGISTRATION = 'ALLOW_REGISTRATION'; 5 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import registerServiceWorker from './registerServiceWorker'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | registerServiceWorker(); 8 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/layout/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export default () => { 5 | return ( 6 | 7 | New 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/components/layout/Spinner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import spinner from './spinner.gif'; 3 | 4 | export default () => { 5 | return ( 6 |
7 | Loading... 12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/components/layout/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Clients from '../clients/Clients'; 3 | import Sidebar from '../layout/Sidebar'; 4 | 5 | export default () => { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/reducers/notifyReducer.js: -------------------------------------------------------------------------------- 1 | import { NOTIFY_USER } from '../actions/types'; 2 | 3 | const initialState = { 4 | message: null, 5 | messageType: null 6 | }; 7 | 8 | export default function(state = initialState, action) { 9 | switch (action.type) { 10 | case NOTIFY_USER: 11 | return { 12 | ...state, 13 | message: action.message, 14 | messageType: action.messageType 15 | }; 16 | default: 17 | return state; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/layout/Alert.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const Alert = props => { 6 | const { message, messageType } = props; 7 | return ( 8 |
14 | {message} 15 |
16 | ); 17 | }; 18 | 19 | Alert.propTypes = { 20 | message: PropTypes.string.isRequired, 21 | messageType: PropTypes.string.isRequired 22 | }; 23 | 24 | export default Alert; 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Client Panel 2 | 3 | > Client management tool built on React, Redux and Firebase. Uses Firebase for authentication and Firestore for storing data 4 | 5 | ## Quick Start 6 | 7 | ```bash 8 | # Install dependencies 9 | npm install 10 | 11 | # ADD YOUR OWN FIREBASE CONFIG TO STORE.JS 12 | 13 | # Serve on localhost:3000 14 | npm start 15 | 16 | # Build for production 17 | npm run build 18 | ``` 19 | 20 | ## App Info 21 | 22 | ### Author 23 | 24 | Brad Traversy 25 | [Traversy Media](http://www.traversymedia.com) 26 | 27 | ### Version 28 | 29 | 1.0.0 30 | 31 | ### License 32 | 33 | This project is licensed under the MIT License 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clientpanel_react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "classnames": "^2.2.6", 7 | "firebase": "^5.3.0", 8 | "react": "^16.4.1", 9 | "react-dom": "^16.4.1", 10 | "react-redux": "^5.0.7", 11 | "react-redux-firebase": "^2.1.6", 12 | "react-router-dom": "^4.3.1", 13 | "react-scripts": "1.1.4", 14 | "redux": "^4.0.0", 15 | "redux-auth-wrapper": "^2.0.3", 16 | "redux-firestore": "^0.5.7" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/reducers/settingsReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | DISABLE_BALANCE_ON_ADD, 3 | DISABLE_BALANCE_ON_EDIT, 4 | ALLOW_REGISTRATION 5 | } from '../actions/types'; 6 | 7 | export default function(state = {}, action) { 8 | switch (action.type) { 9 | case DISABLE_BALANCE_ON_ADD: 10 | return { 11 | ...state, 12 | disableBalanceOnAdd: action.payload 13 | }; 14 | case DISABLE_BALANCE_ON_EDIT: 15 | return { 16 | ...state, 17 | disableBalanceOnEdit: action.payload 18 | }; 19 | case ALLOW_REGISTRATION: 20 | return { 21 | ...state, 22 | allowRegistration: action.payload 23 | }; 24 | default: 25 | return state; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/helpers/auth.js: -------------------------------------------------------------------------------- 1 | import locationHelperBuilder from 'redux-auth-wrapper/history4/locationHelper'; 2 | import { connectedRouterRedirect } from 'redux-auth-wrapper/history4/redirect'; 3 | import LoadingScreen from '../components/layout/Spinner'; 4 | 5 | const locationHelper = locationHelperBuilder({}); 6 | 7 | export const UserIsAuthenticated = connectedRouterRedirect({ 8 | wrapperDisplayName: 'UserIsAuthenticated', 9 | AuthenticatingComponent: LoadingScreen, 10 | allowRedirectBack: true, 11 | redirectPath: (state, ownProps) => 12 | locationHelper.getRedirectQueryParam(ownProps) || '/login', 13 | authenticatingSelector: ({ firebase: { auth, profile, isInitializing } }) => 14 | !auth.isLoaded || isInitializing === true, 15 | authenticatedSelector: ({ firebase: { auth } }) => 16 | auth.isLoaded && !auth.isEmpty 17 | }); 18 | 19 | export const UserIsNotAuthenticated = connectedRouterRedirect({ 20 | wrapperDisplayName: 'UserIsNotAuthenticated', 21 | AuthenticatingComponent: LoadingScreen, 22 | allowRedirectBack: false, 23 | redirectPath: (state, ownProps) => 24 | locationHelper.getRedirectQueryParam(ownProps) || '/', 25 | authenticatingSelector: ({ firebase: { auth, isInitializing } }) => 26 | !auth.isLoaded || isInitializing === true, 27 | authenticatedSelector: ({ firebase: { auth } }) => 28 | auth.isLoaded && auth.isEmpty 29 | }); 30 | -------------------------------------------------------------------------------- /src/actions/settingsActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | DISABLE_BALANCE_ON_ADD, 3 | DISABLE_BALANCE_ON_EDIT, 4 | ALLOW_REGISTRATION 5 | } from './types'; 6 | 7 | export const setDisableBalanceOnAdd = () => { 8 | // Get settings from localStorage 9 | const settings = JSON.parse(localStorage.getItem('settings')); 10 | 11 | // Toggle 12 | settings.disableBalanceOnAdd = !settings.disableBalanceOnAdd; 13 | 14 | // Set back to localStorage 15 | localStorage.setItem('settings', JSON.stringify(settings)); 16 | 17 | return { 18 | type: DISABLE_BALANCE_ON_ADD, 19 | payload: settings.disableBalanceOnAdd 20 | }; 21 | }; 22 | 23 | export const setDisableBalanceOnEdit = () => { 24 | // Get settings from localStorage 25 | const settings = JSON.parse(localStorage.getItem('settings')); 26 | 27 | // Toggle 28 | settings.disableBalanceOnEdit = !settings.disableBalanceOnEdit; 29 | 30 | // Set back to localStorage 31 | localStorage.setItem('settings', JSON.stringify(settings)); 32 | 33 | return { 34 | type: DISABLE_BALANCE_ON_EDIT, 35 | payload: settings.disableBalanceOnEdit 36 | }; 37 | }; 38 | 39 | export const setAllowRegistration = () => { 40 | // Get settings from localStorage 41 | const settings = JSON.parse(localStorage.getItem('settings')); 42 | 43 | // Toggle 44 | settings.allowRegistration = !settings.allowRegistration; 45 | 46 | // Set back to localStorage 47 | localStorage.setItem('settings', JSON.stringify(settings)); 48 | return { 49 | type: ALLOW_REGISTRATION, 50 | payload: settings.allowRegistration 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 16 | 17 | Client Panel 18 | 19 | 20 | 21 | 24 |
25 | 26 | 27 | 29 | 31 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, compose } from 'redux'; 2 | import firebase from 'firebase'; 3 | import 'firebase/firestore'; 4 | import { reactReduxFirebase, firebaseReducer } from 'react-redux-firebase'; 5 | import { reduxFirestore, firestoreReducer } from 'redux-firestore'; 6 | // Reducers 7 | import notifyReducer from './reducers/notifyReducer'; 8 | import settingsReducer from './reducers/settingsReducer'; 9 | 10 | /////// ADD YOUR FIREBASE CONFIG HERE ///////// 11 | const firebaseConfig = {}; 12 | 13 | // react-redux-firebase config 14 | const rrfConfig = { 15 | userProfile: 'users', 16 | useFirestoreForProfile: true // Firestore for Profile instead of Realtime DB 17 | }; 18 | 19 | // Init firebase instance 20 | firebase.initializeApp(firebaseConfig); 21 | // Init firestore 22 | const firestore = firebase.firestore(); 23 | const settings = { timestampsInSnapshots: true }; 24 | firestore.settings(settings); 25 | 26 | // Add reactReduxFirebase enhancer when making store creator 27 | const createStoreWithFirebase = compose( 28 | reactReduxFirebase(firebase, rrfConfig), // firebase instance as first argument 29 | reduxFirestore(firebase) 30 | )(createStore); 31 | 32 | const rootReducer = combineReducers({ 33 | firebase: firebaseReducer, 34 | firestore: firestoreReducer, 35 | notify: notifyReducer, 36 | settings: settingsReducer 37 | }); 38 | 39 | // Check for settings in localStorage 40 | if (localStorage.getItem('settings') == null) { 41 | // Default settings 42 | const defaultSettings = { 43 | disableBalanceOnAdd: true, 44 | disableBalanceOnEdit: false, 45 | allowRegistration: false 46 | }; 47 | 48 | // Set to localStorage 49 | localStorage.setItem('settings', JSON.stringify(defaultSettings)); 50 | } 51 | 52 | // Create initial state 53 | const initialState = { settings: JSON.parse(localStorage.getItem('settings')) }; 54 | 55 | // Create store 56 | const store = createStoreWithFirebase( 57 | rootReducer, 58 | initialState, 59 | compose( 60 | reactReduxFirebase(firebase), 61 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() 62 | ) 63 | ); 64 | 65 | export default store; 66 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 3 | import { UserIsAuthenticated, UserIsNotAuthenticated } from './helpers/auth'; 4 | 5 | import { Provider } from 'react-redux'; 6 | import store from './store'; 7 | 8 | import AppNavbar from './components/layout/AppNavbar'; 9 | import Dashboard from './components/layout/Dashboard'; 10 | import AddClient from './components/clients/AddClient'; 11 | import EditClient from './components/clients/EditClient'; 12 | import ClientDetails from './components/clients/ClientDetails'; 13 | import Login from './components/auth/Login'; 14 | import Register from './components/auth/Register'; 15 | import Settings from './components/settings/Settings'; 16 | 17 | import './App.css'; 18 | 19 | class App extends Component { 20 | render() { 21 | return ( 22 | 23 | 24 |
25 | 26 |
27 | 28 | 33 | 38 | 43 | 48 | 53 | 58 | 63 | 64 |
65 |
66 |
67 |
68 | ); 69 | } 70 | } 71 | 72 | export default App; 73 | -------------------------------------------------------------------------------- /src/components/auth/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { compose } from 'redux'; 4 | import { connect } from 'react-redux'; 5 | import { firebaseConnect } from 'react-redux-firebase'; 6 | import { notifyUser } from '../../actions/notifyActions'; 7 | import Alert from '../layout/Alert'; 8 | 9 | class Login extends Component { 10 | state = { 11 | email: '', 12 | password: '' 13 | }; 14 | 15 | onSubmit = e => { 16 | e.preventDefault(); 17 | 18 | const { firebase, notifyUser } = this.props; 19 | const { email, password } = this.state; 20 | 21 | firebase 22 | .login({ 23 | email, 24 | password 25 | }) 26 | .catch(err => notifyUser('Invalid Login Credentials', 'error')); 27 | }; 28 | 29 | onChange = e => this.setState({ [e.target.name]: e.target.value }); 30 | 31 | render() { 32 | const { message, messageType } = this.props.notify; 33 | return ( 34 |
35 |
36 |
37 |
38 | {message ? ( 39 | 40 | ) : null} 41 |

42 | 43 | Login 44 | 45 |

46 |
47 |
48 | 49 | 57 |
58 |
59 | 60 | 68 |
69 | 74 |
75 |
76 |
77 |
78 |
79 | ); 80 | } 81 | } 82 | 83 | Login.propTypes = { 84 | firebase: PropTypes.object.isRequired, 85 | notify: PropTypes.object.isRequired, 86 | notifyUser: PropTypes.func.isRequired 87 | }; 88 | 89 | export default compose( 90 | firebaseConnect(), 91 | connect( 92 | (state, props) => ({ 93 | notify: state.notify 94 | }), 95 | { notifyUser } 96 | ) 97 | )(Login); 98 | -------------------------------------------------------------------------------- /src/components/clients/Clients.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import { compose } from 'redux'; 5 | import { connect } from 'react-redux'; 6 | import { firestoreConnect } from 'react-redux-firebase'; 7 | import Spinner from '../layout/Spinner'; 8 | 9 | class Clients extends Component { 10 | state = { 11 | totalOwed: null 12 | }; 13 | 14 | static getDerivedStateFromProps(props, state) { 15 | const { clients } = props; 16 | 17 | if (clients) { 18 | // Add balances 19 | const total = clients.reduce((total, client) => { 20 | return total + parseFloat(client.balance.toString()); 21 | }, 0); 22 | 23 | return { totalOwed: total }; 24 | } 25 | 26 | return null; 27 | } 28 | 29 | render() { 30 | const { clients } = this.props; 31 | const { totalOwed } = this.state; 32 | 33 | if (clients) { 34 | return ( 35 |
36 |
37 |
38 |

39 | {' '} 40 | Clients{' '} 41 |

42 |
43 |
44 |
45 | Total Owed{' '} 46 | 47 | ${parseFloat(totalOwed).toFixed(2)} 48 | 49 |
50 |
51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 61 | 62 | 63 | {clients.map(client => ( 64 | 65 | 68 | 69 | 70 | 78 | 79 | ))} 80 | 81 |
NameEmailBalance 60 |
66 | {client.firstName} {client.lastName} 67 | {client.email}${parseFloat(client.balance).toFixed(2)} 71 | 75 | Details 76 | 77 |
82 |
83 | ); 84 | } else { 85 | return ; 86 | } 87 | } 88 | } 89 | 90 | Clients.propTypes = { 91 | firestore: PropTypes.object.isRequired, 92 | clients: PropTypes.array 93 | }; 94 | 95 | export default compose( 96 | firestoreConnect([{ collection: 'clients' }]), 97 | connect((state, props) => ({ 98 | clients: state.firestore.ordered.clients 99 | })) 100 | )(Clients); 101 | -------------------------------------------------------------------------------- /src/components/settings/Settings.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import { connect } from 'react-redux'; 5 | import { 6 | setAllowRegistration, 7 | setDisableBalanceOnAdd, 8 | setDisableBalanceOnEdit 9 | } from '../../actions/settingsActions'; 10 | 11 | class Settings extends Component { 12 | disableBalanceOnAddChange = () => { 13 | const { setDisableBalanceOnAdd } = this.props; 14 | setDisableBalanceOnAdd(); 15 | }; 16 | 17 | disableBalanceOnEditChange = () => { 18 | const { setDisableBalanceOnEdit } = this.props; 19 | setDisableBalanceOnEdit(); 20 | }; 21 | 22 | allowRegistrationChange = () => { 23 | const { setAllowRegistration } = this.props; 24 | setAllowRegistration(); 25 | }; 26 | 27 | render() { 28 | const { 29 | disableBalanceOnAdd, 30 | disableBalanceOnEdit, 31 | allowRegistration 32 | } = this.props.settings; 33 | 34 | return ( 35 |
36 |
37 |
38 | 39 | Back To Dashboard 40 | 41 |
42 |
43 | 44 |
45 |
Edit Settings
46 |
47 |
48 |
49 | {' '} 50 | 56 |
57 | 58 |
59 | {' '} 60 | 66 |
67 | 68 |
69 | {' '} 70 | 76 |
77 |
78 |
79 |
80 |
81 | ); 82 | } 83 | } 84 | 85 | Settings.propTypes = { 86 | settings: PropTypes.object.isRequired, 87 | setDisableBalanceOnAdd: PropTypes.func.isRequired, 88 | setDisableBalanceOnEdit: PropTypes.func.isRequired, 89 | setAllowRegistration: PropTypes.func.isRequired 90 | }; 91 | 92 | export default connect( 93 | (state, props) => ({ 94 | auth: state.firebase.auth, 95 | settings: state.settings 96 | }), 97 | { setAllowRegistration, setDisableBalanceOnAdd, setDisableBalanceOnEdit } 98 | )(Settings); 99 | -------------------------------------------------------------------------------- /src/components/auth/Register.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { compose } from 'redux'; 4 | import { connect } from 'react-redux'; 5 | import { firebaseConnect } from 'react-redux-firebase'; 6 | import { notifyUser } from '../../actions/notifyActions'; 7 | import Alert from '../layout/Alert'; 8 | 9 | class Register extends Component { 10 | state = { 11 | email: '', 12 | password: '' 13 | }; 14 | 15 | componentDidMount() { 16 | const { allowRegistration } = this.props.settings; 17 | 18 | if (!allowRegistration) { 19 | this.props.history.push('/'); 20 | } 21 | } 22 | 23 | onSubmit = e => { 24 | e.preventDefault(); 25 | 26 | const { firebase, notifyUser } = this.props; 27 | const { email, password } = this.state; 28 | 29 | // Register with firebase 30 | firebase 31 | .createUser({ email, password }) 32 | .catch(err => notifyUser('That User Already Exists', 'error')); 33 | }; 34 | 35 | onChange = e => this.setState({ [e.target.name]: e.target.value }); 36 | 37 | render() { 38 | const { message, messageType } = this.props.notify; 39 | return ( 40 |
41 |
42 |
43 |
44 | {message ? ( 45 | 46 | ) : null} 47 |

48 | 49 | Register 50 | 51 |

52 |
53 |
54 | 55 | 63 |
64 |
65 | 66 | 74 |
75 | 80 |
81 |
82 |
83 |
84 |
85 | ); 86 | } 87 | } 88 | 89 | Register.propTypes = { 90 | firebase: PropTypes.object.isRequired, 91 | notify: PropTypes.object.isRequired, 92 | notifyUser: PropTypes.func.isRequired 93 | }; 94 | 95 | export default compose( 96 | firebaseConnect(), 97 | connect( 98 | (state, props) => ({ 99 | notify: state.notify, 100 | settings: state.settings 101 | }), 102 | { notifyUser } 103 | ) 104 | )(Register); 105 | -------------------------------------------------------------------------------- /src/components/layout/AppNavbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import { compose } from 'redux'; 5 | import { connect } from 'react-redux'; 6 | import { firebaseConnect } from 'react-redux-firebase'; 7 | 8 | class AppNavbar extends Component { 9 | state = { 10 | isAuthenticated: false 11 | }; 12 | 13 | static getDerivedStateFromProps(props, state) { 14 | const { auth } = props; 15 | 16 | if (auth.uid) { 17 | return { isAuthenticated: true }; 18 | } else { 19 | return { isAuthenticated: false }; 20 | } 21 | } 22 | 23 | onLogoutClick = e => { 24 | e.preventDefault(); 25 | 26 | const { firebase } = this.props; 27 | firebase.logout(); 28 | }; 29 | 30 | render() { 31 | const { isAuthenticated } = this.state; 32 | const { auth } = this.props; 33 | const { allowRegistration } = this.props.settings; 34 | 35 | return ( 36 | 100 | ); 101 | } 102 | } 103 | 104 | AppNavbar.propTypes = { 105 | firebase: PropTypes.object.isRequired, 106 | auth: PropTypes.object.isRequired, 107 | settings: PropTypes.object.isRequired 108 | }; 109 | 110 | export default compose( 111 | firebaseConnect(), 112 | connect((state, props) => ({ 113 | auth: state.firebase.auth, 114 | settings: state.settings 115 | })) 116 | )(AppNavbar); 117 | -------------------------------------------------------------------------------- /src/components/clients/AddClient.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import { compose } from 'redux'; 5 | import { connect } from 'react-redux'; 6 | import { firestoreConnect } from 'react-redux-firebase'; 7 | 8 | class AddClient extends Component { 9 | state = { 10 | firstName: '', 11 | lastName: '', 12 | email: '', 13 | phone: '', 14 | balance: '' 15 | }; 16 | 17 | onSubmit = e => { 18 | e.preventDefault(); 19 | 20 | const newClient = this.state; 21 | 22 | const { firestore, history } = this.props; 23 | 24 | // If no balance, make 0 25 | if (newClient.balance === '') { 26 | newClient.balance = 0; 27 | } 28 | 29 | firestore 30 | .add({ collection: 'clients' }, newClient) 31 | .then(() => history.push('/')); 32 | }; 33 | 34 | onChange = e => this.setState({ [e.target.name]: e.target.value }); 35 | 36 | render() { 37 | const { disableBalanceOnAdd } = this.props.settings; 38 | 39 | return ( 40 |
41 |
42 |
43 | 44 | Back To Dashboard 45 | 46 |
47 |
48 | 49 |
50 |
Add Client
51 |
52 |
53 |
54 | 55 | 64 |
65 | 66 |
67 | 68 | 77 |
78 | 79 |
80 | 81 | 88 |
89 | 90 |
91 | 92 | 101 |
102 | 103 |
104 | 105 | 113 |
114 | 115 | 120 |
121 |
122 |
123 |
124 | ); 125 | } 126 | } 127 | 128 | AddClient.propTypes = { 129 | firestore: PropTypes.object.isRequired, 130 | settings: PropTypes.object.isRequired 131 | }; 132 | 133 | export default compose( 134 | firestoreConnect(), 135 | connect((state, props) => ({ 136 | settings: state.settings 137 | })) 138 | )(AddClient); 139 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/components/clients/EditClient.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import { compose } from 'redux'; 5 | import { connect } from 'react-redux'; 6 | import { firestoreConnect } from 'react-redux-firebase'; 7 | import Spinner from '../layout/Spinner'; 8 | 9 | class EditClient extends Component { 10 | constructor(props) { 11 | super(props); 12 | // Create refs 13 | this.firstNameInput = React.createRef(); 14 | this.lastNameInput = React.createRef(); 15 | this.emailInput = React.createRef(); 16 | this.phoneInput = React.createRef(); 17 | this.balanceInput = React.createRef(); 18 | } 19 | 20 | onSubmit = e => { 21 | e.preventDefault(); 22 | 23 | const { client, firestore, history } = this.props; 24 | 25 | // Updated Client 26 | const updClient = { 27 | firstName: this.firstNameInput.current.value, 28 | lastName: this.lastNameInput.current.value, 29 | email: this.emailInput.current.value, 30 | phone: this.phoneInput.current.value, 31 | balance: 32 | this.balanceInput.current.value === '' 33 | ? 0 34 | : this.balanceInput.current.value 35 | }; 36 | 37 | // Update client in firestore 38 | firestore 39 | .update({ collection: 'clients', doc: client.id }, updClient) 40 | .then(history.push('/')); 41 | }; 42 | 43 | render() { 44 | const { client } = this.props; 45 | const { disableBalanceOnEdit } = this.props.settings; 46 | 47 | if (client) { 48 | return ( 49 |
50 |
51 |
52 | 53 | Back To Dashboard 54 | 55 |
56 |
57 | 58 |
59 |
Edit Client
60 |
61 |
62 |
63 | 64 | 73 |
74 | 75 |
76 | 77 | 86 |
87 | 88 |
89 | 90 | 97 |
98 | 99 |
100 | 101 | 110 |
111 | 112 |
113 | 114 | 122 |
123 | 124 | 129 |
130 |
131 |
132 |
133 | ); 134 | } else { 135 | return ; 136 | } 137 | } 138 | } 139 | 140 | EditClient.propTypes = { 141 | firestore: PropTypes.object.isRequired 142 | }; 143 | 144 | export default compose( 145 | firestoreConnect(props => [ 146 | { collection: 'clients', storeAs: 'client', doc: props.match.params.id } 147 | ]), 148 | connect(({ firestore: { ordered }, settings }, props) => ({ 149 | client: ordered.client && ordered.client[0], 150 | settings 151 | })) 152 | )(EditClient); 153 | -------------------------------------------------------------------------------- /src/components/clients/ClientDetails.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import { compose } from 'redux'; 5 | import { connect } from 'react-redux'; 6 | import { firestoreConnect } from 'react-redux-firebase'; 7 | import Spinner from '../layout/Spinner'; 8 | import classnames from 'classnames'; 9 | 10 | class ClientDetails extends Component { 11 | state = { 12 | showBalanceUpdate: false, 13 | balanceUpdateAmount: '' 14 | }; 15 | 16 | // Update balance 17 | balanceSubmit = e => { 18 | e.preventDefault(); 19 | 20 | const { client, firestore } = this.props; 21 | const { balanceUpdateAmount } = this.state; 22 | 23 | const clientUpdate = { 24 | balance: parseFloat(balanceUpdateAmount) 25 | }; 26 | 27 | // Update in firestore 28 | firestore.update({ collection: 'clients', doc: client.id }, clientUpdate); 29 | }; 30 | 31 | // Delete client 32 | onDeleteClick = () => { 33 | const { client, firestore, history } = this.props; 34 | 35 | firestore 36 | .delete({ collection: 'clients', doc: client.id }) 37 | .then(history.push('/')); 38 | }; 39 | 40 | onChange = e => this.setState({ [e.target.name]: e.target.value }); 41 | 42 | render() { 43 | const { client } = this.props; 44 | const { showBalanceUpdate, balanceUpdateAmount } = this.state; 45 | 46 | let balanceForm = ''; 47 | // If balance form should display 48 | if (showBalanceUpdate) { 49 | balanceForm = ( 50 |
51 |
52 | 60 |
61 | 66 |
67 |
68 |
69 | ); 70 | } else { 71 | balanceForm = null; 72 | } 73 | 74 | if (client) { 75 | return ( 76 |
77 |
78 |
79 | 80 | Back To Dashboard 81 | 82 |
83 |
84 |
85 | 86 | Edit 87 | 88 | 91 |
92 |
93 |
94 |
95 |
96 |

97 | {client.firstName} {client.lastName} 98 |

99 |
100 |
101 |
102 |

103 | Client ID:{' '} 104 | {client.id} 105 |

106 |
107 |
108 |

109 | Balance:{' '} 110 | 0, 113 | 'text-success': client.balance === 0 114 | })} 115 | > 116 | ${parseFloat(client.balance).toFixed(2)} 117 | {' '} 118 | 119 | 122 | this.setState({ 123 | showBalanceUpdate: !this.state.showBalanceUpdate 124 | }) 125 | } 126 | > 127 | 128 | 129 | 130 |

131 | {balanceForm} 132 |
133 |
134 | 135 |
136 |
    137 |
  • 138 | Contact Email: {client.email} 139 |
  • 140 |
  • 141 | Contact Phone: {client.phone} 142 |
  • 143 |
144 |
145 |
146 |
147 | ); 148 | } else { 149 | return ; 150 | } 151 | } 152 | } 153 | 154 | ClientDetails.propTypes = { 155 | firestore: PropTypes.object.isRequired 156 | }; 157 | 158 | export default compose( 159 | firestoreConnect(props => [ 160 | { collection: 'clients', storeAs: 'client', doc: props.match.params.id } 161 | ]), 162 | connect(({ firestore: { ordered } }, props) => ({ 163 | client: ordered.client && ordered.client[0] 164 | })) 165 | )(ClientDetails); 166 | --------------------------------------------------------------------------------