├── src
├── index.css
├── pages
│ ├── deal
│ │ ├── deal.styles.scss
│ │ └── deal.page.jsx
│ ├── contact
│ │ ├── contact.styles.scss
│ │ └── contact.page.jsx
│ ├── deal-list
│ │ ├── deal-list.styles.scss
│ │ └── deal-list.page.jsx
│ ├── sign-in-and-sign-up
│ │ ├── sign-in-and-sign-up.styles.scss
│ │ └── sign-in-and-sign-up.page.jsx
│ ├── contact-form
│ │ ├── contact-form.styles.scss
│ │ └── contact-form.page.jsx
│ ├── deal-form
│ │ ├── deal-form.styles.scss
│ │ └── deal-form.page.jsx
│ └── contact-list
│ │ ├── contact-list.styles.scss
│ │ └── contact-list.page.jsx
├── components
│ ├── table
│ │ ├── table.styles.scss
│ │ └── table.component.jsx
│ ├── suggestions
│ │ ├── suggestions.styles.scss
│ │ └── suggestions.component.jsx
│ ├── home
│ │ └── home.component.jsx
│ ├── sign-up
│ │ ├── sign-up.styles.scss
│ │ └── sign-up.component.jsx
│ ├── deal
│ │ ├── deal.styles.scss
│ │ └── deal.component.jsx
│ ├── edit-input
│ │ ├── edit-input.styles.scss
│ │ └── edit-input.component.jsx
│ ├── sign-in
│ │ ├── sign-in.styles.scss
│ │ └── sign-in.component.jsx
│ ├── deal-column
│ │ ├── deal-column.styles.scss
│ │ └── deal-column.component.jsx
│ ├── custom-button
│ │ ├── custom-button.component.jsx
│ │ └── custom-button.styles.scss
│ ├── form-input
│ │ ├── form-input.component.jsx
│ │ └── form-input.styles.scss
│ └── header
│ │ ├── header.styles.scss
│ │ └── header.component.jsx
├── jsconfig.json
├── redux
│ ├── user
│ │ ├── user.types.js
│ │ ├── user.actions.js
│ │ └── user.reducer.js
│ ├── root-reducer.js
│ └── store.js
├── App.css
├── setupTests.js
├── App.test.js
├── index.js
├── logo.svg
├── firebase
│ └── firebase.utils.js
├── assets
│ └── crown.svg
├── App.js
└── serviceWorker.js
├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── .firebaserc
├── firebase.json
├── .gitignore
├── .firebase
├── hosting.cHVibGlj.cache
└── hosting.YnVpbGQ.cache
├── README.md
├── .github
└── workflows
│ ├── firebase-hosting-merge.yml
│ └── firebase-hosting-pull-request.yml
└── package.json
/src/index.css:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/pages/deal/deal.styles.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/table/table.styles.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/contact/contact.styles.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/table/table.component.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "crm-cms-pjatk"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6"
4 | }
5 | }
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adbo/CRM-React-Firebase/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adbo/CRM-React-Firebase/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adbo/CRM-React-Firebase/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/src/redux/user/user.types.js:
--------------------------------------------------------------------------------
1 | export const UserActionTypes = {
2 | SET_CURRENT_USER: 'SET_CURRENT_USER'
3 | }
--------------------------------------------------------------------------------
/src/components/suggestions/suggestions.styles.scss:
--------------------------------------------------------------------------------
1 | .suggestions {
2 | ul {
3 | list-style: none;
4 | }
5 | }
--------------------------------------------------------------------------------
/src/pages/deal-list/deal-list.styles.scss:
--------------------------------------------------------------------------------
1 | .list-container {
2 | padding: 2rem 0rem;
3 | display: grid;
4 | grid-auto-flow: column;
5 | }
--------------------------------------------------------------------------------
/src/pages/sign-in-and-sign-up/sign-in-and-sign-up.styles.scss:
--------------------------------------------------------------------------------
1 | .sign-in-and-sign-up {
2 | width: 850px;
3 | display: flex;
4 | justify-content: space-between;
5 | margin: 30px auto;
6 | }
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "build",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ]
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/home/home.component.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Home = () => (
4 |
5 | Home Page
6 |
7 | );
8 |
9 | export default Home;
--------------------------------------------------------------------------------
/src/components/sign-up/sign-up.styles.scss:
--------------------------------------------------------------------------------
1 | .sign-up {
2 | display: flex;
3 | flex-direction: column;
4 | width: 380px;
5 |
6 | .title {
7 | margin: 10px 0;
8 | }
9 | }
--------------------------------------------------------------------------------
/src/redux/root-reducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import userReducer from './user/user.reducer';
4 |
5 | export default combineReducers({
6 | user: userReducer
7 | });
--------------------------------------------------------------------------------
/src/redux/user/user.actions.js:
--------------------------------------------------------------------------------
1 | import { UserActionTypes } from './user.types'
2 |
3 | export const setCurrentUser = user => ({
4 | type: UserActionTypes.SET_CURRENT_USER,
5 | payload: user
6 | });
--------------------------------------------------------------------------------
/src/components/deal/deal.styles.scss:
--------------------------------------------------------------------------------
1 | .deal {
2 | padding: .75rem .75rem;
3 | margin: .25rem;
4 | border: 1px solid lightgrey;
5 | border-radius: 1rem;
6 |
7 | h4 {
8 | margin: 0;
9 | }
10 | }
--------------------------------------------------------------------------------
/src/components/edit-input/edit-input.styles.scss:
--------------------------------------------------------------------------------
1 | .edit-input {
2 | width: 10rem;
3 |
4 | .buttons {
5 | display: flex;
6 | text-align: right;
7 | margin: .3rem 0;
8 | justify-content: flex-end;
9 | }
10 | }
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Roboto', sans-serif;
3 | padding: 0px 40px;
4 | background: #f7f7f7;
5 | }
6 |
7 | a {
8 | text-decoration: none;
9 | color: black;
10 | }
11 |
12 | * {
13 | box-sizing: border-box;
14 | }
--------------------------------------------------------------------------------
/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/components/sign-in/sign-in.styles.scss:
--------------------------------------------------------------------------------
1 | .sign-in {
2 | width: 380px;
3 | display: flex;
4 | flex-direction: column;
5 |
6 | .title {
7 | margin: 10px 0;
8 | }
9 |
10 | .buttons {
11 | display: flex;
12 | justify-content: space-between;
13 | }
14 | }
--------------------------------------------------------------------------------
/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import logger from 'redux-logger';
3 |
4 | import rootReducer from './root-reducer';
5 |
6 | const middlewares = [logger];
7 |
8 | const store = createStore(rootReducer, applyMiddleware(...middlewares));
9 |
10 | export default store;
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | const { getByText } = render( );
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/src/components/deal-column/deal-column.styles.scss:
--------------------------------------------------------------------------------
1 | .column-title {
2 | font-weight: bold;
3 | padding: 1rem 1rem;
4 | border-top: 1px solid lightgrey;
5 | border-right: 1px solid lightgrey;
6 | border-bottom: 1px solid lightgrey;
7 | }
8 |
9 | .column-title:first-of-type {
10 | border-left: 1px solid lightgrey;
11 | }
--------------------------------------------------------------------------------
/src/pages/contact-form/contact-form.styles.scss:
--------------------------------------------------------------------------------
1 | .contact-form {
2 | width: 30vw;
3 | display: flex;
4 | flex-direction: column;
5 | margin: 30px auto;
6 |
7 | .title {
8 | margin: 10px 0;
9 | }
10 |
11 | .buttons {
12 | display: flex;
13 | justify-content: center;
14 | }
15 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 |
5 | import store from './redux/store';
6 |
7 | import App from './App';
8 |
9 | import './index.css';
10 |
11 | ReactDOM.render(
12 |
13 |
14 |
15 | , document.getElementById('root'));
16 |
--------------------------------------------------------------------------------
/src/components/custom-button/custom-button.component.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import './custom-button.styles.scss';
4 |
5 | const CustomButton = ({ children, customClass, ...otherProps }) => (
6 |
10 | {children}
11 |
12 | );
13 |
14 | export default CustomButton;
--------------------------------------------------------------------------------
/src/pages/deal-form/deal-form.styles.scss:
--------------------------------------------------------------------------------
1 | .deal-form {
2 | background-color: white;
3 | padding: 2rem 2rem;
4 | width: 30vw;
5 | display: flex;
6 | flex-direction: column;
7 | margin: 30px auto;
8 |
9 | h2 {
10 | margin-block-start: 0;
11 | }
12 |
13 | .title {
14 | margin: 10px 0;
15 | }
16 |
17 | .buttons {
18 | display: flex;
19 | justify-content: center;
20 | }
21 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/src/pages/sign-in-and-sign-up/sign-in-and-sign-up.page.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import SignIn from '../../components/sign-in/sign-in.component';
4 | import SignUp from '../../components/sign-up/sign-up.component';
5 |
6 | import './sign-in-and-sign-up.styles.scss';
7 |
8 | const SignInAndSignUp = () => (
9 |
10 |
11 |
12 |
13 | );
14 |
15 | export default SignInAndSignUp;
--------------------------------------------------------------------------------
/src/redux/user/user.reducer.js:
--------------------------------------------------------------------------------
1 | import { UserActionTypes } from './user.types'
2 |
3 | const INITIAL_STATE = {
4 | currentUser: null
5 | };
6 |
7 | const userReducer = (state = INITIAL_STATE, action) => {
8 | switch (action.type) {
9 | case UserActionTypes.SET_CURRENT_USER:
10 | return {
11 | ...state,
12 | currentUser: action.payload
13 | }
14 | default:
15 | return state;
16 | }
17 | }
18 |
19 | export default userReducer;
--------------------------------------------------------------------------------
/src/components/deal-column/deal-column.component.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Deal from '../deal/deal.component';
4 |
5 | import './deal-column.styles.scss';
6 |
7 | const DealColumn = props =>
8 |
9 |
{props.status.name}
10 | {props.data.map(deal =>
11 |
12 | )}
13 |
14 |
15 | export default DealColumn;
--------------------------------------------------------------------------------
/src/components/deal/deal.component.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | import './deal.styles.scss';
5 |
6 | const Deal = ({ deal }) => {
7 | return (
8 |
9 |
10 |
{deal.title}
11 |
{deal.name}
12 |
${deal.dealValue}
13 |
14 |
15 | );
16 | }
17 |
18 |
19 | export default Deal;
--------------------------------------------------------------------------------
/src/components/suggestions/suggestions.component.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import './suggestions.styles.scss';
4 |
5 | const Suggestions = ({ suggestions, setContact }) => {
6 | return (
7 |
8 |
9 | {
10 | suggestions ?
11 | suggestions.map(item =>
12 | setContact(item)} key={item.name} item={item}>{item.name} )
13 | : null
14 | }
15 |
16 |
17 | )
18 | }
19 |
20 | export default Suggestions;
--------------------------------------------------------------------------------
/src/components/form-input/form-input.component.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import './form-input.styles.scss';
4 |
5 | const FormInput = ({ handleChange, label, ...otherProps }) => {
6 | return (
7 |
8 |
9 | {label ?
10 | (
11 | {label}
12 | )
13 | : null
14 | }
15 |
16 | )
17 | }
18 |
19 | export default FormInput;
--------------------------------------------------------------------------------
/.firebase/hosting.cHVibGlj.cache:
--------------------------------------------------------------------------------
1 | favicon.ico,1625168678625,c599b7a91ab3627e3538125d9f40adc2d4bf949046984262670545dc7738af06
2 | logo192.png,1625168678625,76c449ccb9cd117c2f2338f091b18f7050f3210e249b2228f5c81b23f34377cd
3 | logo512.png,1625168678626,7779210d56c1f3741e2e487799fe3092def4fa6ac450a60532b807c3a8971205
4 | manifest.json,1625168678626,0958a5e0c831126100c8c2d06a6bbaa665a3900f21aaff4130238a6f5a113aa1
5 | robots.txt,1625168678626,9486af852ce71d8ddbb9bc5bd6f7a3e45740f8c66ee724d1ae2bcc4dd7d3ded0
6 | index.html,1625171348214,58284037f6fa8823f9734c278e39113fd492021aa7501892cac656e33394f50d
7 |
--------------------------------------------------------------------------------
/src/components/header/header.styles.scss:
--------------------------------------------------------------------------------
1 | .header {
2 | height: 70px;
3 | width: 100%;
4 | display: flex;
5 | justify-content: space-between;
6 | margin-bottom: 25px;
7 |
8 | .logo-container {
9 | height: 100%;
10 | width: 70px;
11 | padding: 5px;
12 | }
13 |
14 | .options {
15 | width: 50%;
16 | height: 100%;
17 | display: flex;
18 | align-items: center;
19 | justify-content: flex-end;
20 |
21 | .option {
22 | padding: 10px 15px;
23 | cursor: pointer;
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Simple CRM application written with React.
2 |
3 | Features:
4 | - authentication with Firebase Authentication
5 | - persistent data storage with Firestore Database
6 | - state management with Redux
7 | - CI/CD with Github Actions
8 | - adding and storing information about clients, contacts and deals
9 |
10 | Prerequisites:
11 | - Node.JS installed
12 |
13 | Instalation:
14 | ```
15 | npm install
16 | ```
17 |
18 | Run:
19 | ```
20 | npm start
21 | ```
22 |
23 | Using:
24 | - On login page click "Sign in with Google" or sign up with email address
25 |
26 | Live demo:
27 | https://crm-cms-pjatk.web.app/
--------------------------------------------------------------------------------
/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 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/.github/workflows/firebase-hosting-merge.yml:
--------------------------------------------------------------------------------
1 | # This file was auto-generated by the Firebase CLI
2 | # https://github.com/firebase/firebase-tools
3 |
4 | name: Deploy to Firebase Hosting on merge
5 | 'on':
6 | push:
7 | branches:
8 | - master
9 | jobs:
10 | build_and_deploy:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - run: npm ci && npm run build
15 | - uses: FirebaseExtended/action-hosting-deploy@v0
16 | with:
17 | repoToken: '${{ secrets.GITHUB_TOKEN }}'
18 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_CRM_CMS_PJATK }}'
19 | channelId: live
20 | projectId: crm-cms-pjatk
21 |
--------------------------------------------------------------------------------
/.github/workflows/firebase-hosting-pull-request.yml:
--------------------------------------------------------------------------------
1 | # This file was auto-generated by the Firebase CLI
2 | # https://github.com/firebase/firebase-tools
3 |
4 | name: Deploy to Firebase Hosting on PR
5 | 'on': pull_request
6 | jobs:
7 | build_and_preview:
8 | if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - run: npm ci && npm run build
13 | - uses: FirebaseExtended/action-hosting-deploy@v0
14 | with:
15 | repoToken: '${{ secrets.GITHUB_TOKEN }}'
16 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_CRM_CMS_PJATK }}'
17 | projectId: crm-cms-pjatk
18 |
--------------------------------------------------------------------------------
/src/components/edit-input/edit-input.component.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import CustomButton from '../../components/custom-button/custom-button.component';
4 |
5 | import './edit-input.styles.scss';
6 |
7 | const EditInput = ({ handleSubmit, name, value}) => {
8 | return (
9 |
18 | )
19 | }
20 |
21 | export default EditInput;
--------------------------------------------------------------------------------
/src/pages/contact-list/contact-list.styles.scss:
--------------------------------------------------------------------------------
1 | .contact-list-table {
2 | padding: 2rem 0rem;
3 |
4 | .table {
5 | width: 100%;
6 | display: table;
7 | border-spacing: 0;
8 | border-color: grey;
9 |
10 | thead {
11 | display: table-header-group;
12 | vertical-align: middle;
13 | border-color: inherit;
14 | text-align: inherit;
15 | }
16 |
17 | tbody {
18 | display: table-row-group;
19 | vertical-align: middle;
20 | border-color: inherit;
21 |
22 | tr:first-child {
23 | td {
24 | border-top: 0;
25 | }
26 | }
27 | }
28 |
29 | th {
30 | padding: .75rem;
31 | vertical-align: top;
32 | border-top: 1px solid #dee2e6;
33 | border-bottom: 2px solid #dee2e6;
34 | text-align: inherit;
35 | }
36 |
37 | td {
38 | padding: .75rem;
39 | vertical-align: top;
40 | border-top: 1px solid #dee2e6;
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/src/pages/deal/deal.page.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useParams } from 'react-router-dom';
3 |
4 | import { getDocuments } from '../../firebase/firebase.utils';
5 |
6 | import './deal.styles.scss';
7 |
8 | const Deal = () => {
9 | const type = 'deal';
10 |
11 | const [dealState, setDealState] = useState({});
12 | const { dealId } = useParams();
13 |
14 | useEffect(() => {
15 | const getData = async () => {
16 | const snapShot = await getDocuments(dealId, type);
17 | const deal = snapShot.data();
18 | setDealState(deal);
19 | }
20 | getData();
21 | }, [dealId]);
22 |
23 | return (
24 |
25 | Deal Page
26 |
{dealState.title}
27 |
Name: {dealState.name}
28 |
Organization: {dealState.company}
29 |
DealValue: {dealState.dealValue}
30 |
Date: {dealState.date}
31 |
32 | );
33 | }
34 |
35 | export default Deal;
--------------------------------------------------------------------------------
/src/components/header/header.component.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { connect } from 'react-redux';
4 |
5 | import { auth } from '../../firebase/firebase.utils';
6 |
7 | import { ReactComponent as Logo } from '../../assets/crown.svg';
8 |
9 | import './header.styles.scss';
10 |
11 | const Header = ({ currentUser }) => (
12 |
13 |
14 |
15 |
16 |
17 |
Deals
18 |
Contacts
19 | {
20 | currentUser ?
21 |
auth.signOut()}>Sign Out
22 | :
23 |
Sign In
24 | }
25 |
26 |
27 | );
28 |
29 | const mapStateToProps = state => ({
30 | currentUser: state.user.currentUser
31 | });
32 |
33 | export default connect(mapStateToProps)(Header);
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "crm",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.4.0",
8 | "@testing-library/user-event": "^7.2.1",
9 | "firebase": "^7.8.0",
10 | "node-sass": "^4.13.1",
11 | "react": "^16.12.0",
12 | "react-dom": "^16.12.0",
13 | "react-redux": "^7.1.3",
14 | "react-router-dom": "^5.1.2",
15 | "react-scripts": "3.4.1",
16 | "redux": "^4.0.5",
17 | "redux-logger": "^3.0.6"
18 | },
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "test": "react-scripts test",
23 | "eject": "react-scripts eject"
24 | },
25 | "eslintConfig": {
26 | "extends": "react-app"
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.2%",
31 | "not dead",
32 | "not op_mini all"
33 | ],
34 | "development": [
35 | "last 1 chrome version",
36 | "last 1 firefox version",
37 | "last 1 safari version"
38 | ]
39 | },
40 | "devDependencies": {
41 | "eslint": "^6.8.0",
42 | "prettier": "^1.19.1"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/form-input/form-input.styles.scss:
--------------------------------------------------------------------------------
1 | $sub-color: grey;
2 | $main-color: black;
3 |
4 | @mixin shrinkLabel {
5 | top: -14px;
6 | font-size: 12px;
7 | color: $main-color;
8 | }
9 |
10 | .group {
11 | position: relative;
12 | margin: 40px 0;
13 |
14 | .form-input {
15 | background: none;
16 | background-color: white;
17 | color: $sub-color;
18 | font-family: inherit;
19 | font-size: 18px;
20 | padding: 10px 10px 10px 5px;
21 | display: block;
22 | width: 100%;
23 | border: none;
24 | border-radius: 0;
25 | border-bottom: 1px solid $sub-color;
26 | margin: 25px 0;
27 |
28 | &:focus {
29 | outline: none;
30 | }
31 |
32 | &:focus ~ .form-input-label {
33 | @include shrinkLabel();
34 | }
35 | }
36 |
37 | input[type='password'] {
38 | letter-spacing: 0.3em;
39 | }
40 |
41 | .form-input-label {
42 | color: $sub-color;
43 | font-size: 16px;
44 | font-weight: normal;
45 | position: absolute;
46 | pointer-events: none;
47 | left: 5px;
48 | top: 10px;
49 | transition: 300ms ease all;
50 |
51 | &.shrink {
52 | @include shrinkLabel();
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/src/pages/contact-list/contact-list.page.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | import { getDocuments } from '../../firebase/firebase.utils';
5 |
6 | import CustomButton from '../../components/custom-button/custom-button.component';
7 |
8 | import './contact-list.styles.scss';
9 |
10 | const ContactList = () => {
11 | const type = 'contact';
12 |
13 | const [contactListState, setContactListState] = useState([]);
14 |
15 | useEffect(() => {
16 | const getData = async () => {
17 | const snapShot = await getDocuments(null, type);
18 | const contacts = snapShot.docs.map(doc => ({
19 | id: doc.id,
20 | ...doc.data()
21 | }));
22 | setContactListState(contacts);
23 | }
24 | getData();
25 | }, []);
26 |
27 | const getTableData = contacts =>
28 | contacts.map(contact => (
29 |
30 | {contact.name}
31 | {contact.company}
32 | {contact.email}
33 | {contact.position}
34 |
35 | ));
36 |
37 | return (
38 |
39 |
40 |
Add person
41 |
42 |
43 |
44 |
45 |
46 | Name
47 | Company
48 | Email
49 | Position
50 |
51 |
52 |
53 | {getTableData(contactListState)}
54 |
55 |
56 |
57 |
58 | )
59 | }
60 |
61 | export default ContactList;
--------------------------------------------------------------------------------
/src/pages/deal-list/deal-list.page.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | import { getDocuments } from '../../firebase/firebase.utils';
5 |
6 | import CustomButton from '../../components/custom-button/custom-button.component';
7 | import DealColumn from '../../components/deal-column/deal-column.component';
8 |
9 | import './deal-list.styles.scss';
10 |
11 | const DealList = () => {
12 | const type = 'deal';
13 | const statuses = [
14 | {
15 | value: 0,
16 | name: 'Lead In'
17 | },
18 | {
19 | value: 1,
20 | name: 'Contact made'
21 | },
22 | {
23 | value: 2,
24 | name: 'Demo scheduled'
25 | },
26 | {
27 | value: 3,
28 | name: 'Proposal made'
29 | },
30 | {
31 | value: 4,
32 | name: 'Negotiations started'
33 | }
34 | ];
35 | const [dealState, setDealState] = useState([]);
36 |
37 | useEffect(() => {
38 | const getData = async () => {
39 | const snapShot = await getDocuments(null, type);
40 | const deals = snapShot.docs.map(doc => ({
41 | id: doc.id,
42 | ...doc.data()
43 | }));
44 | setDealState(deals);
45 | console.log(deals);
46 | }
47 | getData();
48 | }, []);
49 |
50 | return (
51 |
52 |
53 |
Add deal
54 |
55 |
56 | {
57 | statuses.map(status =>
58 | deal.status === status.value)}
62 | />
63 | )
64 | }
65 |
66 |
67 | )
68 | }
69 |
70 | export default DealList;
--------------------------------------------------------------------------------
/src/components/sign-in/sign-in.component.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import CustomButton from '../custom-button/custom-button.component';
4 | import FormInput from '../form-input/form-input.component';
5 | import { auth, signInWithGoogle } from '../../firebase/firebase.utils';
6 |
7 | import './sign-in.styles.scss'
8 |
9 | const SignIn = () => {
10 | const [signInState, setSignInState] = useState({
11 | email: '',
12 | password: ''
13 | });
14 |
15 | const handleSubmit = async event => {
16 | event.preventDefault();
17 |
18 | const { email, password } = signInState;
19 |
20 | try {
21 | await auth.signInWithEmailAndPassword(email, password);
22 | setSignInState({
23 | email: '',
24 | password: ''
25 | });
26 | } catch (error) {
27 | console.error(error);
28 | }
29 | }
30 |
31 | const handleChange = event => {
32 | const { name, value } = event.target;
33 | setSignInState({
34 | ...signInState,
35 | [name]: value
36 | })
37 | }
38 |
39 | return (
40 |
41 |
I have an account
42 |
Sign in with your email and password
43 |
65 |
66 | )
67 | };
68 |
69 | export default SignIn;
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
19 |
20 |
29 | React App
30 |
31 |
32 | You need to enable JavaScript to run this app.
33 |
34 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/.firebase/hosting.YnVpbGQ.cache:
--------------------------------------------------------------------------------
1 | 404.html,1625171752728,b7bab6b83fa074653ff28c8d2a64135d3434575f70a12ab3d3ba8080461b9537
2 | asset-manifest.json,1625171194193,f7c9bb0da75f4b186647532d249657be36b2eac03aa1c57ebe9aca1afe73aa9d
3 | favicon.ico,1625171182588,c599b7a91ab3627e3538125d9f40adc2d4bf949046984262670545dc7738af06
4 | index.html,1625171194193,bf525bf5dad05b467d0bdf98d1fba94f5d6b1b8e474d5b56695e30818feb9ed3
5 | logo192.png,1625171182589,76c449ccb9cd117c2f2338f091b18f7050f3210e249b2228f5c81b23f34377cd
6 | manifest.json,1625171182589,0958a5e0c831126100c8c2d06a6bbaa665a3900f21aaff4130238a6f5a113aa1
7 | precache-manifest.d34bc30d9b55d132bde14deed5167568.js,1625171194193,30fa5f0a0d4e239be6715a61738289c107242df643a43fef8e2c0a68bc83a155
8 | robots.txt,1625171182589,9486af852ce71d8ddbb9bc5bd6f7a3e45740f8c66ee724d1ae2bcc4dd7d3ded0
9 | service-worker.js,1625171194193,0f3a91d47a69c65b84d0ca636a1f9840b28208f5ef6e8a6d54d5bd00748073d3
10 | logo512.png,1625171182589,7779210d56c1f3741e2e487799fe3092def4fa6ac450a60532b807c3a8971205
11 | static/css/main.d4437381.chunk.css,1625171194194,1bb5df31479ad0c38bc96c2acc1bdcd7108b4a1fe4703542972a3213475ea256
12 | static/js/2.8ba9031e.chunk.js.LICENSE.txt,1625171194214,d88efc11c9db0ccb3e4cd6d3b547551ceb1123daa2732b1c1188b4a2229a8696
13 | static/css/main.d4437381.chunk.css.map,1625171194214,e1c6822db0b9dbbff0eeddc23f07ce3625c0716e434f4e81bd95a69ff4b12a9b
14 | static/js/main.1bb546dc.chunk.js,1625171194194,26d9bf8aee0a88f1491af1c038ea1799377efc35dae850cc66257ea01dd2aef9
15 | static/js/runtime-main.160ced06.js,1625171194214,4433c30a6c39de79ef54df16417dd6c7930c97a8bcef54956b28475b90e27545
16 | static/js/runtime-main.160ced06.js.map,1625171194214,16b35db9b025201b3e96c20ef8f33457ba17e5dc6f02b5ff3b890a1c0c5fc00d
17 | static/media/crown.0509381b.svg,1625171194194,b1916f00e52ed639d681edb195a52fc718ac65d892a5c3e3d6212b7b63540dd5
18 | static/js/main.1bb546dc.chunk.js.map,1625171194214,855276a22972b052a1571cacb1253cb2fd00beff435476c092202afff4a071f2
19 | static/js/2.8ba9031e.chunk.js,1625171194214,34bc218b9f8424788e147be49a5a249e2435c3776db1bd8a89845471c988ab79
20 | static/js/2.8ba9031e.chunk.js.map,1625171194214,ef7d39d3fccd7b4678c735cbb1d53bf2583528f962645e1e08629deeb2fd1da1
21 |
--------------------------------------------------------------------------------
/src/pages/contact/contact.page.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useParams } from 'react-router-dom';
3 |
4 | import Deal from '../../components/deal/deal.component';
5 | //import EditInput from '../../components/edit-input/edit-input.component'
6 |
7 | import { getDocuments } from '../../firebase/firebase.utils';
8 |
9 | const Contact = () => {
10 |
11 | const [contactState, setContactState] = useState({});
12 | const [contactDealState, setContactDealState] = useState(null);
13 | const { contactId } = useParams();
14 |
15 | useEffect(() => {
16 | const getContacts = async () => {
17 | const type = 'contact';
18 | const snapShot = await getDocuments(contactId, type);
19 | const contact = snapShot.data();
20 | setContactState(contact);
21 | }
22 | getContacts();
23 | }, [contactId]);
24 |
25 | useEffect(() => {
26 | const getDeals = async contact => {
27 | const type = 'deal';
28 | const where = {
29 | field: 'name',
30 | operator: '==',
31 | condition: contact.name
32 | }
33 | const snapShot = await getDocuments(null, type, null, where);
34 | const deals = snapShot.docs;
35 | let dealList = []
36 | for(let deal of deals) {
37 | dealList.push({
38 | id: deal.id,
39 | ...deal.data()
40 | });
41 | }
42 | setContactDealState(dealList);
43 | }
44 | if (contactState.name)
45 | getDeals(contactState);
46 | }, [contactState]);
47 |
48 | return (
49 |
50 |
51 | Contact Page
52 |
{contactState.name}
53 |
Organization: {contactState.company}
54 |
Email: {contactState.email}
55 |
Position: {contactState.position}
56 |
57 |
58 |
Deals:
59 | {
60 | contactDealState ?
61 | contactDealState.map(
62 | deal =>
63 | (
64 |
65 | )
66 | )
67 | : null
68 | }
69 |
70 |
71 | )};
72 |
73 | export default Contact;
--------------------------------------------------------------------------------
/src/pages/contact-form/contact-form.page.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { createDocument } from '../../firebase/firebase.utils';
4 |
5 | import FormInput from '../../components/form-input/form-input.component';
6 | import CustomButton from '../../components/custom-button/custom-button.component';
7 |
8 | import './contact-form.styles.scss';
9 |
10 |
11 | const ContactForm = props => {
12 | const type = 'contact';
13 |
14 | const [contactsFormState, setContactsFormState] = useState({
15 | name: '',
16 | company: '',
17 | email: '',
18 | position: ''
19 | });
20 |
21 | const handleSubmit = async event => {
22 | event.preventDefault();
23 |
24 | const contactRef = await createDocument(contactsFormState, type);
25 | props.history.push(`/${type}/${contactRef.id}`);
26 | }
27 |
28 | const handleChange = event => {
29 | const { name, value } = event.target;
30 | setContactsFormState({
31 | ...contactsFormState,
32 | [name]: value
33 | })
34 | }
35 |
36 | return (
37 |
38 |
Add person
39 |
Provide information about your new contact
40 |
73 |
74 | );
75 | }
76 |
77 | export default ContactForm;
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/custom-button/custom-button.styles.scss:
--------------------------------------------------------------------------------
1 | .custom-button {
2 | min-width: 165px;
3 | width: auto;
4 | height: 50px;
5 | letter-spacing: 0.5px;
6 | line-height: 50px;
7 | padding: 0 35px 0 35px;
8 | font-size: 15px;
9 | background-color: black;
10 | color: white;
11 | text-transform: uppercase;
12 | font-family: 'Open Sans Condensed';
13 | font-weight: bolder;
14 | border: none;
15 | cursor: pointer;
16 |
17 | &:hover {
18 | background-color: white;
19 | color: black;
20 | border: 1px solid black;
21 | }
22 |
23 | &.google-sign-in {
24 | background-color: #4285f4;
25 | color: white;
26 |
27 | &:hover {
28 | background-color: #357ae8;
29 | border: none;
30 | }
31 | }
32 |
33 | &.action-button {
34 | background-color: #00A82D;
35 | color: white;
36 | border-radius: 5px;
37 | font-family: 'Roboto', sans-serif;
38 |
39 | &:hover {
40 | background-color: #01ce38;
41 | border: none;
42 | }
43 |
44 | & a {
45 | color: white;
46 | }
47 | }
48 |
49 | &.small-green {
50 | background-color: #00A82D;
51 | color: white;
52 | min-width: 3rem;
53 | width: auto;
54 | height: 1.5rem;
55 | letter-spacing: .5px;
56 | padding: 0;
57 | margin: 0;
58 | font-family: 'Roboto', sans-serif;
59 | font-size: .7rem;
60 | line-height: 0;
61 | display: inline-flex;
62 | justify-content: center;
63 | align-items: center;
64 | text-align: center;
65 | position: relative;
66 | border-radius: .15rem;
67 |
68 | &:hover {
69 | background-color: #01ce38;
70 | border: none;
71 | }
72 |
73 | & a {
74 | color: white;
75 | }
76 | }
77 |
78 | &.small-white {
79 | background-color: white;
80 | color: black;
81 | min-width: 3rem;
82 | width: auto;
83 | height: 1.5rem;
84 | letter-spacing: .5px;
85 | padding: 0 0.5rem;
86 | margin: 0 .3rem;
87 | font-family: 'Roboto', sans-serif;
88 | font-size: .7rem;
89 | line-height: 0;
90 | display: inline-flex;
91 | justify-content: center;
92 | align-items: center;
93 | text-align: center;
94 | position: relative;
95 | border-radius: .15rem;
96 | border: 1px solid grey;
97 |
98 | &:hover {
99 | background-color: white;
100 | color: black;
101 | }
102 |
103 | & a {
104 | color: white;
105 | }
106 | }
107 | }
--------------------------------------------------------------------------------
/src/firebase/firebase.utils.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase/app';
2 | import 'firebase/auth';
3 | import 'firebase/firestore';
4 |
5 | const config = {
6 | apiKey: "AIzaSyCuCIjUTExp2r4zPRaBkzxli5XDV8gVnwM",
7 | authDomain: "crm-cms-pjatk.firebaseapp.com",
8 | databaseURL: "https://crm-cms-pjatk.firebaseio.com",
9 | projectId: "crm-cms-pjatk",
10 | storageBucket: "crm-cms-pjatk.appspot.com",
11 | messagingSenderId: "1075490656124",
12 | appId: "1:1075490656124:web:0a8ae178e1675052bddef9",
13 | measurementId: "G-GK6SBY3BQJ"
14 | };
15 |
16 | export const createDocument = async (document, type) => {
17 | if(!document) return;
18 |
19 | let contactRef = null;
20 |
21 | try {
22 | contactRef = await firestore.collection(type).add({
23 | ...document
24 | });
25 | } catch (error) {
26 | console.error(`error adding contact ${type}`, error)
27 | }
28 |
29 | return contactRef;
30 | }
31 |
32 | export const getDocuments = async (documentId=null, type, orderBy=null, whereFrom=null, whereTo=null) => {
33 | let snapShot = null;
34 | let docRef = null;
35 |
36 | try {
37 | docRef = await firestore.collection(type);
38 |
39 | if (documentId)
40 | docRef = await docRef.doc(documentId);
41 |
42 | if (orderBy)
43 | docRef = await docRef.orderBy(orderBy);
44 |
45 | if (whereFrom)
46 | docRef = await docRef.where(whereFrom.field, whereFrom.operator, whereFrom.condition);
47 |
48 | if (whereTo)
49 | docRef = await docRef.where(whereTo.field, whereTo.operator, whereTo.condition);
50 |
51 | console.log(docRef);
52 | snapShot = await docRef.get();
53 | } catch (error) {
54 | console.error(`error retrieving ${type} `, error);
55 | }
56 |
57 | return snapShot;
58 | }
59 |
60 | export const createUserProfileDocument = async (userAuth, additionalData) => {
61 | if(!userAuth) return;
62 |
63 | const userRef = firestore.doc(`users/${userAuth.uid}`);
64 |
65 | const snapShot = await userRef.get();
66 |
67 | if(!snapShot.exists) {
68 | const { displayName, email } = userAuth;
69 | const createdAt = new Date();
70 |
71 | try {
72 | await userRef.set({
73 | displayName,
74 | email,
75 | createdAt,
76 | ...additionalData
77 | })
78 | } catch (error) {
79 | console.log('error creating user ', error);
80 | }
81 | }
82 |
83 | return userRef;
84 | }
85 |
86 | firebase.initializeApp(config);
87 |
88 | export const auth = firebase.auth();
89 | export const firestore = firebase.firestore();
90 |
91 | const provider = new firebase.auth.GoogleAuthProvider();
92 | provider.setCustomParameters({ prompt: 'select_account' });
93 | export const signInWithGoogle = () => auth.signInWithPopup(provider);
94 |
95 | export default firebase;
--------------------------------------------------------------------------------
/src/components/sign-up/sign-up.component.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import FormInput from '../form-input/form-input.component';
4 | import CustomButton from '../custom-button/custom-button.component';
5 |
6 | import { auth, createUserProfileDocument } from '../../firebase/firebase.utils';
7 |
8 | import './sign-up.styles.scss';
9 |
10 | const SignUp = () => {
11 | const [signUpState, setSignUpState] = useState({
12 | displayName: '',
13 | email: '',
14 | password: '',
15 | confirmPassword: ''
16 | });
17 |
18 | const handleSubmit = async event => {
19 | event.preventDefault();
20 |
21 | const { displayName, email, password, confirmPassword } = signUpState;
22 |
23 | if (password !== confirmPassword) {
24 | alert("passwords don't match");
25 | return;
26 | }
27 |
28 | try {
29 | const { user } = await auth.createUserWithEmailAndPassword(
30 | email,
31 | password
32 | );
33 |
34 | await createUserProfileDocument(user, { displayName });
35 |
36 | setSignUpState({
37 | displayName: '',
38 | email: '',
39 | password: '',
40 | confirmPassword: ''
41 | });
42 | } catch (error) {
43 | console.error(error);
44 | }
45 | };
46 |
47 | const handleChange = event => {
48 | const { name, value } = event.target;
49 |
50 | setSignUpState({
51 | ...signUpState,
52 | [name]: value
53 | })
54 | };
55 |
56 | return (
57 |
58 |
I do not have an account
59 | Sign up with your email and password
60 |
95 |
96 | );
97 | }
98 |
99 | export default SignUp;
--------------------------------------------------------------------------------
/src/assets/crown.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Svg Vector Icons : http://www.onlinewebfonts.com/icon
6 |
7 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
3 | import { connect } from 'react-redux';
4 |
5 | import { auth, createUserProfileDocument } from './firebase/firebase.utils';
6 | import { setCurrentUser } from './redux/user/user.actions';
7 |
8 | import Contact from './pages/contact/contact.page';
9 | import ContactList from './pages/contact-list/contact-list.page';
10 | import ContactsForm from './pages/contact-form/contact-form.page';
11 | import Deal from './pages/deal/deal.page';
12 | import DealList from './pages/deal-list/deal-list.page';
13 | import DealForm from './pages/deal-form/deal-form.page';
14 | import Header from './components/header/header.component';
15 | //import Home from './components/home/home.component';
16 | import SignInAndSignUp from './pages/sign-in-and-sign-up/sign-in-and-sign-up.page';
17 |
18 | import './App.css';
19 |
20 | const App = props => {
21 |
22 | const { currentUser, setCurrentUser } = props;
23 |
24 | useEffect(() => {
25 | const unsubsribe = auth.onAuthStateChanged(async userAuth => {
26 | if (userAuth) {
27 | const userRef = await createUserProfileDocument(userAuth);
28 | userRef.onSnapshot(snapShot =>
29 | setCurrentUser({
30 | id: snapShot.id,
31 | ...snapShot.data()
32 | })
33 | );
34 | } else {
35 | setCurrentUser(userAuth);
36 | }
37 | });
38 |
39 | return () => unsubsribe();
40 | }, [setCurrentUser]);
41 |
42 | const PrivateRoute = ({ component: Component, ...rest }) => (
43 | (
44 | currentUser
45 | ?
46 | :
47 | )} />
48 | )
49 |
50 | return (
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
65 | currentUser ?
66 |
67 | :
68 |
69 | }
70 | />
71 |
72 |
73 | );
74 | }
75 |
76 | const mapStateToProps = state => ({
77 | currentUser: state.user.currentUser
78 | });
79 |
80 | const mapDispatchToProps = dispatch => ({
81 | setCurrentUser: user => dispatch(setCurrentUser(user))
82 | });
83 |
84 | export default connect(
85 | mapStateToProps,
86 | mapDispatchToProps
87 | )(App);
88 |
--------------------------------------------------------------------------------
/src/pages/deal-form/deal-form.page.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { createDocument, getDocuments } from '../../firebase/firebase.utils';
4 |
5 | import FormInput from '../../components/form-input/form-input.component';
6 | import CustomButton from '../../components/custom-button/custom-button.component';
7 | import Suggestions from '../../components/suggestions/suggestions.component';
8 |
9 | import './deal-form.styles.scss';
10 |
11 | const DealForm = props => {
12 | const type = 'deal';
13 |
14 | const [dealFormState, setDealFormState] = useState({
15 | name: '',
16 | company: '',
17 | title: '',
18 | dealValue: '',
19 | date: '',
20 | status: 0
21 | });
22 |
23 | const [suggestionsState, setSuggestionsState] = useState(null);
24 |
25 | const [dateFocus, setDateFocus] = useState(false);
26 |
27 | const handleSubmit = async event => {
28 | event.preventDefault();
29 |
30 | const contactRef = await createDocument(dealFormState, type);
31 | props.history.push(`/${type}/${contactRef.id}`);
32 | }
33 |
34 | const handleChange = event => {
35 | if (event === null || event.target === undefined) {
36 | setDealFormState({
37 | ...dealFormState,
38 | date: event
39 | })
40 | } else {
41 | const { name, value } = event.target;
42 | setDealFormState({
43 | ...dealFormState,
44 | [name]: value
45 | });
46 | }
47 | }
48 |
49 | const handleFocus = () => {
50 | setDateFocus(!dateFocus);
51 | }
52 |
53 | const handleNameChange = async event => {
54 | const { name, value } = event.target;
55 |
56 | setDealFormState({
57 | ...dealFormState,
58 | [name]: value
59 | });
60 |
61 | let docList = [];
62 | const getContacts = async name => {
63 | const type = 'contact';
64 | const whereFrom = {
65 | field: 'name',
66 | operator: '>=',
67 | condition: name
68 | }
69 |
70 | const whereTo = {
71 | field: 'name',
72 | operator: '<=',
73 | condition: name + '\uf8ff'
74 | }
75 |
76 | const snapShot = await getDocuments(null, type, null, whereFrom, whereTo);
77 | const docs = snapShot.docs;
78 | for(let doc of docs) {
79 | docList.push(doc.data());
80 | }
81 | }
82 |
83 | if(value.length === 2)
84 | await getContacts(value);
85 |
86 | setSuggestionsState(docList);
87 | }
88 |
89 | const setContact = contact => {
90 | setDealFormState({
91 | ...dealFormState,
92 | name: contact.name,
93 | company: contact.company,
94 | title: `${contact.company} deal`
95 | });
96 |
97 | setSuggestionsState(null);
98 | }
99 |
100 | return (
101 |
102 |
Add deal
103 |
Provide information about your new deal
104 |
146 |
147 | );
148 | }
149 |
150 | export default DealForm;
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' }
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready.then(registration => {
134 | registration.unregister();
135 | });
136 | }
137 | }
138 |
--------------------------------------------------------------------------------