CRM or the Customer Relationship Management system is a great way to administrate the relationship with the customer. It’s a great tool to know and a pure pleasure to build.
In this article, we will discuss how to create a CRM app using React.js
, Material UI
, we will be also using other integrations such as Asana to create a ticket whenever a ticket is create on our app update a Google Sheet whenever a deal is added to the user.
Let's get started
- Setting up React
Let's begin by creating a boilerplate using the
create-react-app
npx create-react-app crm-app
- Install Material UI
We will be using Material UI as an UI library. It boosts up the speed of development as we won't have to much of CSS manually.
npm install @mui/material @emotion/react @emotion/styled @material-ui/icons @mui/icons-material
- Create a ContentTable component
We are using a Table component by Material UI to quickly set up a table with pagination support, we will be using Custom pagination actions
you can read more about it here.
This will give us a separate component. That handles pagination, for the sake of simplicity we will be extracting that function into another component.
To create your first component, create a directory inside src directory and call it components. Proceed by creating another directory inside ‘components’ and we will name it ContentTable, we will create a file with the same name inside of it, ContentTable.js.
const customers = [
{
id: 1,
name: "Lillian Carter",
email: "xcollier@goodwin.com",
phone: "+1-267-551-8666",
company: "Larkin Group",
label: "Marketing",
},
{
id: 2,
name: "Otto Walker",
email: "stokes.hubert@hotmail.com",
phone: "+1-580-977-4361",
company: "Bednar-Sawayn",
label: "Newsletter",
},
{
id: 3,
name: "Kaylee Taylor",
email: "diana45@hotmail.com",
phone: "+1-202-918-2132",
company: "Rolfson and Sons ",
label: "Ads",
},
{
id: 4,
name: "Aiden Houston",
email: "ctromp@kassulke.info",
phone: "+1-215-480-3687",
company: "Wisoky, Windler and Nienow",
label: "Newsletter",
},
{
id: 5,
name: "Davis Houston",
email: "voreilly@yahoo.com",
phone: "+1-203-883-5460",
company: "Schmidt, Streich and Schuster",
label: "Ads",
},
];
src/components/ListCustomer/ListCustomer.js
And, will pretty much copy the code for Table from Material UI’s documentation, and we will loop through our static array to fill the data in the table.
import { React, useState } from "react";
import {
Table,
TableBody,
TableContainer,
TableFooter,
TablePagination,
TableRow,
Paper,
TableCell,
TableHead,
} from "@mui/material";
const customers = [
{
id: 1,
name: "Lillian Carter",
email: "xcollier@goodwin.com",
phone: "+1-267-551-8666",
company: "Larkin Group",
label: "Marketing",
},
{
id: 2,
name: "Otto Walker",
email: "stokes.hubert@hotmail.com",
phone: "+1-580-977-4361",
company: "Bednar-Sawayn",
label: "Newsletter",
},
{
id: 3,
name: "Kaylee Taylor",
email: "diana45@hotmail.com",
phone: "+1-202-918-2132",
company: "Rolfson and Sons ",
label: "Ads",
},
{
id: 4,
name: "Aiden Houston",
email: "ctromp@kassulke.info",
phone: "+1-215-480-3687",
company: "Wisoky, Windler and Nienow",
label: "Newsletter",
},
{
id: 5,
name: "Davis Houston",
email: "voreilly@yahoo.com",
phone: "+1-203-883-5460",
company: "Schmidt, Streich and Schuster",
label: "Ads",
},
];
const ContentTable = () => {
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
const emptyRows =
page > 0 ? Math.max(0, (1 + page) * rowsPerPage - customers.length) : 0;
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
return (
<TableContainer component={Paper} sx={{ margin: "2rem", width: "95%" }}>
<Table sx={{ minWidth: 500 }} aria-label="custom pagination table">
<TableHead>
<TableRow>
<TableCell
align="left"
sx={{
backgroundColor: "black",
color: "white",
borderRight: "1px solid white",
}}
>
Name
</TableCell>
<TableCell
align="left"
sx={{
backgroundColor: "black",
color: "white",
borderRight: "1px solid white",
}}
>
Company
</TableCell>
<TableCell
align="left"
sx={{
backgroundColor: "black",
color: "white",
borderRight: "1px solid white",
}}
>
Email
</TableCell>
<TableCell
align="left"
sx={{
backgroundColor: "black",
color: "white",
borderRight: "1px solid white",
}}
>
Phone
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{customers &&
(rowsPerPage > 0
? customers.slice(
page * rowsPerPage,
page * rowsPerPage + rowsPerPage
)
: customers
).map((row, index) => (
<TableRow key={index}>
<TableCell
component="th"
scope="row"
sx={{ width: 160, borderRight: "1px solid black" }}
>
{row.name}
</TableCell>
<TableCell
sx={{ width: 160, borderRight: "1px solid black" }}
align="left"
>
{row.company}
</TableCell>
<TableCell
sx={{ width: 160, borderRight: "1px solid black" }}
align="left"
>
{row.email}
</TableCell>
<TableCell sx={{ width: 160 }} align="left">
{row.phone}
</TableCell>
</TableRow>
))}
{emptyRows > 0 && (
<TableRow style={{ height: 53 * emptyRows }}>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25, { label: "All", value: -1 }]}
colSpan={3}
count={customers}
rowsPerPage={rowsPerPage}
page={page}
SelectProps={{
inputProps: {
"aria-label": "rows per page",
},
native: true,
}}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
ActionsComponent={TablePaginationActions}
/>
</TableRow>
</TableFooter>
</Table>
</TableContainer>
);
};
export default ContentTable;
src/components/ContentTable/ContentTable.js
The above code will complain about TablePaginationActions
being undefined, so let’s handle that.
Next, we will create a utility component, that will help us with Pagination, Material UI uses it on same component as a demo, but extracting it outside makes ContentTable bit more clean.
Create a directory in componenets
and name it Pagination and create a index.js file inside of it.
import { React } from "react";
import PropTypes from "prop-types";
import { useTheme } from "@mui/material/styles";
import { Box, IconButton } from "@mui/material";
import FirstPageIcon from "@mui/icons-material/FirstPage";
import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft";
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
import LastPageIcon from "@mui/icons-material/LastPage";
export const TablePaginationActions = (props) => {
const theme = useTheme();
const { count, page, rowsPerPage, onPageChange } = props;
const handleFirstPageButtonClick = (event) => {
onPageChange(event, 0);
};
const handleBackButtonClick = (event) => {
onPageChange(event, page - 1);
};
const handleNextButtonClick = (event) => {
onPageChange(event, page + 1);
};
const handleLastPageButtonClick = (event) => {
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
};
return (
<Box sx={{ flexShrink: 0, ml: 2.5 }}>
<IconButton
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
>
{theme.direction === "rtl" ? <LastPageIcon /> : <FirstPageIcon />}
</IconButton>
<IconButton
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
>
{theme.direction === "rtl" ? (
<KeyboardArrowRight />
) : (
<KeyboardArrowLeft />
)}
</IconButton>
<IconButton
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
>
{theme.direction === "rtl" ? (
<KeyboardArrowLeft />
) : (
<KeyboardArrowRight />
)}
</IconButton>
<IconButton
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
{theme.direction === "rtl" ? <FirstPageIcon /> : <LastPageIcon />}
</IconButton>
</Box>
);
};
TablePaginationActions.propTypes = {
count: PropTypes.number.isRequired,
onPageChange: PropTypes.func.isRequired,
page: PropTypes.number.isRequired,
rowsPerPage: PropTypes.number.isRequired,
};
src/components/Pagination/index.js
We can now import this Pagination Component in the ContentTable component and the error will go away. The end result should look like this
- Create a Modal component
Now our table is ready, let’s work Modal component, we want this modal to pop up anytime a user clicks on any of the row on the table. We will be using Material UI’s Modal component for this you can read more about it here
In a modal, we want 3 columns:
- 1st column would display more information about the particular customer
- 2nd column would accommodate the notes feature, where a user would be able to leave notes on the customer.
- 3rd column would a feature to add a deal to user and create a task on Asana.
It would look something like this
Let’s get to it.
import { React } from "react";
import { styled } from "@mui/material/styles";
import { Typography, Fade, Modal, Grid, Paper, Backdrop } from "@mui/material";
const Item = styled(Paper)(({ theme }) => ({
...theme.typography.body2,
padding: theme.spacing(1),
textAlign: "center",
color: theme.palette.text.secondary,
boxShadow: "none",
}));
export default function Modals() {
return (
<div>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={open}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<Fade in={open}>
<Grid
container
spacing={1}
sx={{
width: "95%",
height: "95%",
backgroundColor: "white",
position: "absolute",
top: "4%",
left: "3%",
}}
>
<Grid item xs={3} sx={{ padding: "5px" }}>
<Typography variant="h6" gutterBottom component="div">
Name
</Typography>
<Typography variant="h6" gutterBottom component="div">
Company
</Typography>
<Typography variant="h6" gutterBottom component="div">
Phone
</Typography>
<Typography variant="h6" gutterBottom component="div">
Email
</Typography>
<Typography variant="h6" gutterBottom component="div">
Label
</Typography>
</Grid>
<Grid
item
xs={6}
sx={{ backgroundColor: "lightgray", padding: "5px" }}
>
<Item sx={{ backgroundColor: "inherit", marginTop: "2rem" }}>
<Typography variant="h6" gutterBottom component="div">
Notes
</Typography>
</Item>
</Grid>
<Grid item xs={3} sx={{ padding: "5px", marginTop: "2rem" }}>
<Item>
<Typography variant="h6" gutterBottom component="div">
Deals
</Typography>
<Typography variant="p" gutterBottom component="div">
Track the revenue opportunities associated with this record
</Typography>
</Item>
<Item sx={{ marginTop: "2rem" }}>
<Typography variant="h6" gutterBottom component="div">
Tickets
</Typography>
<Typography variant="p" gutterBottom component="div">
Track the customer requests associated with this record
</Typography>
</Item>
</Grid>
</Grid>
</Fade>
</Modal>
</div>
);
}
src/components/Modal/Modal.js
We are heavily utilizing the Material UI’s Grid layout to make this column structure you can get more read on it here
The end result would look like this
But you have no way to see this, since nothing is triggering this component to open up. Let get that done.
We need to do 4 things in ContentTable component now:
- Import the Modal component.
- Create a state to hold the status of whether Modal is open or not.
- Create a onClick trigger on the table’s row.
- Feed the customer’s data to Modal component.
import Modals from "../Modal";
const [isOpen, setIsOpen] = useState();
const [customerData, setCustomerData] = useState();
const handleClick = (data) => {
setIsOpen(true);
setCustomerData(data);
};
<TableRow
key={index}
onClick={() => {
handleClick(row); //We are passing customer's information though this 'row' parameter
}}
sx={{ cursor: "pointer" }}
>
<Modals data={customerData} open={isOpen} setIsOpen={setIsOpen} />
Now our task of setting up the modal is done. This is how the ContentTable component should look like:
import { React, useState } from "react";
import {
Table,
TableBody,
TableContainer,
TableFooter,
TablePagination,
TableRow,
Paper,
TableCell,
TableHead,
} from "@mui/material";
import { TablePaginationActions } from "../Pagination";
import Modals from "../Modal";
const customers = [
{
id: 1,
name: "Lillian Carter",
email: "xcollier@goodwin.com",
phone: "+1-267-551-8666",
company: "Larkin Group",
label: "Marketing",
},
{
id: 2,
name: "Otto Walker",
email: "stokes.hubert@hotmail.com",
phone: "+1-580-977-4361",
company: "Bednar-Sawayn",
label: "Newsletter",
},
{
id: 3,
name: "Kaylee Taylor",
email: "diana45@hotmail.com",
phone: "+1-202-918-2132",
company: "Rolfson and Sons ",
label: "Ads",
},
{
id: 4,
name: "Aiden Houston",
email: "ctromp@kassulke.info",
phone: "+1-215-480-3687",
company: "Wisoky, Windler and Nienow",
label: "Newsletter",
},
{
id: 5,
name: "Davis Houston",
email: "voreilly@yahoo.com",
phone: "+1-203-883-5460",
company: "Schmidt, Streich and Schuster",
label: "Ads",
},
];
const ContentTable = () => {
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
const [isOpen, setIsOpen] = useState();
const [customerData, setCustomerData] = useState();
const emptyRows =
page > 0 ? Math.max(0, (1 + page) * rowsPerPage - customers.length) : 0;
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
const handleClick = (data) => {
setIsOpen(true);
setCustomerData(data);
};
return (
<TableContainer component={Paper} sx={{ margin: "2rem", width: "95%" }}>
<Table sx={{ minWidth: 500 }} aria-label="custom pagination table">
<TableHead>
<TableRow>
<TableCell
align="left"
sx={{
backgroundColor: "black",
color: "white",
borderRight: "1px solid white",
}}
>
Name
</TableCell>
<TableCell
align="left"
sx={{
backgroundColor: "black",
color: "white",
borderRight: "1px solid white",
}}
>
Company
</TableCell>
<TableCell
align="left"
sx={{
backgroundColor: "black",
color: "white",
borderRight: "1px solid white",
}}
>
Email
</TableCell>
<TableCell
align="left"
sx={{
backgroundColor: "black",
color: "white",
borderRight: "1px solid white",
}}
>
Phone
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{customers &&
(rowsPerPage > 0
? customers.slice(
page * rowsPerPage,
page * rowsPerPage + rowsPerPage
)
: customers
).map((row, index) => (
<TableRow
key={index}
onClick={() => {
handleClick(row);
}}
sx={{ cursor: "pointer" }}
>
<TableCell
component="th"
scope="row"
sx={{ width: 160, borderRight: "1px solid black" }}
>
{row.name}
</TableCell>
<TableCell
sx={{ width: 160, borderRight: "1px solid black" }}
align="left"
>
{row.company}
</TableCell>
<TableCell
sx={{ width: 160, borderRight: "1px solid black" }}
align="left"
>
{row.email}
</TableCell>
<TableCell sx={{ width: 160 }} align="left">
{row.phone}
</TableCell>
</TableRow>
))}
{emptyRows > 0 && (
<TableRow style={{ height: 53 * emptyRows }}>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25, { label: "All", value: -1 }]}
colSpan={3}
count={customers}
rowsPerPage={rowsPerPage}
page={page}
SelectProps={{
inputProps: {
"aria-label": "rows per page",
},
native: true,
}}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
ActionsComponent={TablePaginationActions}
/>
</TableRow>
</TableFooter>
</Table>
<Modals data={customerData} open={isOpen} setIsOpen={setIsOpen} />
</TableContainer>
);
};
export default ContentTable;
src/components/ContentTable/ContentTable.js
Now if you click on any row on the table, the Modal we created should pop-up.
Now we are receiving 3 props in Modal component,
- data - that contains the data of that row’s customer
- open - is a state containing a boolean value of whether modal is open or not
- setIsOpen - which is used to manipulate the value of ‘open’
Let’s utilize this.
export default function Modals({ data, open, setIsOpen })
const handleClose = () => {
setIsOpen(!open);
};
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={open}
onClose={handleClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<Grid item xs={3} sx={{ padding: "5px" }}>
<Typography variant="h6" gutterBottom component="div">
{data.name}
</Typography>
<Typography variant="h6" gutterBottom component="div">
{data.company}
</Typography>
<Typography variant="h6" gutterBottom component="div">
{data.phone}
</Typography>
<Typography variant="h6" gutterBottom component="div">
{data.email}
</Typography>
<Typography variant="h6" gutterBottom component="div">
{data.label}
</Typography>
</Grid>
Now our Modal component should look like this:
import { React } from "react";
import { styled } from "@mui/material/styles";
import { Typography, Fade, Modal, Grid, Paper, Backdrop } from "@mui/material";
const Item = styled(Paper)(({ theme }) => ({
...theme.typography.body2,
padding: theme.spacing(1),
textAlign: "center",
color: theme.palette.text.secondary,
boxShadow: "none",
}));
export default function Modals({ data, open, setIsOpen }) {
const handleClose = () => {
setIsOpen(!open);
};
return (
<div>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={open}
onClose={handleClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<Fade in={open}>
<Grid
container
spacing={1}
sx={{
width: "95%",
height: "95%",
backgroundColor: "white",
position: "absolute",
top: "4%",
left: "3%",
}}
>
<Grid item xs={3} sx={{ padding: "5px" }}>
<Typography variant="h6" gutterBottom component="div">
**{data.name}**
</Typography>
<Typography variant="h6" gutterBottom component="div">
**{data.company}**
</Typography>
<Typography variant="h6" gutterBottom component="div">
**{data.phone}**
</Typography>
<Typography variant="h6" gutterBottom component="div">
**{data.email}**
</Typography>
<Typography variant="h6" gutterBottom component="div">
**{data.label}**
</Typography>
</Grid>
<Grid
item
xs={6}
sx={{ backgroundColor: "lightgray", padding: "5px" }}
>
<Item sx={{ backgroundColor: "inherit", marginTop: "2rem" }}>
<Typography variant="h6" gutterBottom component="div">
Notes
</Typography>
</Item>
</Grid>
<Grid item xs={3} sx={{ padding: "5px", marginTop: "2rem" }}>
<Item>
<Typography variant="h6" gutterBottom component="div">
Deals
</Typography>
<Typography variant="p" gutterBottom component="div">
Track the revenue opportunities associated with this record
</Typography>
</Item>
<Item sx={{ marginTop: "2rem" }}>
<Typography variant="h6" gutterBottom component="div">
Tickets
</Typography>
<Typography variant="p" gutterBottom component="div">
Track the customer requests associated with this record
</Typography>
</Item>
</Grid>
</Grid>
</Fade>
</Modal>
</div>
);
}
src/components/Modal/Modal.js
The modal should have the dynamic data based on which row you click, and should look something like this:
- Create a DetailsCard component.
Our bare-bones modal is ready, but it doesn’t look good at all, let’s extract out user’s information to a separate component, so we can enhance the looks.
Proceed by creating DetailsCard directory inside components, and create a DetailsCards.js inside it. We will be using Material UI’s Card component, you can read more about it here
import React from "react";
import { Fab, Typography, CardContent, Card } from "@mui/material";
const DetailsCard = () => {
return (
<>
<Card sx={{ minWidth: 250, marginBottom: "2rem", marginTop: "2rem" }}>
<CardContent>
<Typography
variant="body"
gutterBottom
component="div"
sx={{ textAlign: "center", marginBottom: "1rem" }}
>
Personal Details
</Typography>
<Typography variant="h5" gutterBottom component="div">
Name
</Typography>
<Typography variant="h6" gutterBottom component="div">
Last Name
</Typography>
</CardContent>
</Card>
<Card sx={{ minWidth: 250, marginBottom: "2rem" }}>
<CardContent>
<Typography
variant="body"
gutterBottom
component="div"
sx={{ textAlign: "center", marginBottom: "1rem" }}
>
Professional Details
</Typography>
<Typography variant="h6" gutterBottom component="div">
Company
</Typography>
<Typography variant="h6" gutterBottom component="div">
Email
</Typography>
</CardContent>
</Card>
<Card sx={{ minWidth: 250 }}>
<CardContent>
<Typography
variant="body"
gutterBottom
component="div"
sx={{ textAlign: "center", marginBottom: "1rem" }}
>
Labels
</Typography>
<Fab variant="extended" size="small">
Labels
</Fab>
</CardContent>
</Card>
</>
);
};
export default DetailsCard;
src/components/DetailsCard/DetailsCard.js
All we have to do now is import and feed the customer data as props in Modal component
The Modal component would look like this after importing and feeding in the customer data
import { React } from "react";
import { styled } from "@mui/material/styles";
import { Typography, Fade, Modal, Grid, Paper, Backdrop } from "@mui/material";
import DetailsCard from "../DetailsCard";
const Item = styled(Paper)(({ theme }) => ({
...theme.typography.body2,
padding: theme.spacing(1),
textAlign: "center",
color: theme.palette.text.secondary,
boxShadow: "none",
}));
export default function Modals({ data, open, setIsOpen }) {
const handleClose = () => {
setIsOpen(!open);
};
return (
<div>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={open}
onClose={handleClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<Fade in={open}>
<Grid
container
spacing={1}
sx={{
width: "95%",
height: "95%",
backgroundColor: "white",
position: "absolute",
top: "4%",
left: "3%",
}}
>
<Grid item xs={3} sx={{ padding: "5px" }}>
<DetailsCard
name={data.name}
company={data.company}
email={data.email}
phone={data.phone}
labels={data.labels}
/>
</Grid>
<Grid
item
xs={6}
sx={{ backgroundColor: "lightgray", padding: "5px" }}
>
<Item sx={{ backgroundColor: "inherit", marginTop: "2rem" }}>
<Typography variant="h6" gutterBottom component="div">
Notes
</Typography>
</Item>
</Grid>
<Grid item xs={3} sx={{ padding: "5px", marginTop: "2rem" }}>
<Item>
<Typography variant="h6" gutterBottom component="div">
Deals
</Typography>
<Typography variant="p" gutterBottom component="div">
Track the revenue opportunities associated with this record
</Typography>
</Item>
<Item sx={{ marginTop: "2rem" }}>
<Typography variant="h6" gutterBottom component="div">
Tickets
</Typography>
<Typography variant="p" gutterBottom component="div">
Track the customer requests associated with this record
</Typography>
</Item>
</Grid>
</Grid>
</Fade>
</Modal>
</div>
);
}
src/components/Modal/Modal.js
And, after utilizing the received props, the DetailsCard component should look like this
import React from "react";
import { Fab, Typography, CardContent, Card } from "@mui/material";
const DetailsCard = ({ name, company, email, phone, labels }) => {
return (
<>
<Card sx={{ minWidth: 250, marginBottom: "2rem", marginTop: "2rem" }}>
<CardContent>
<Typography
variant="body"
gutterBottom
component="div"
sx={{ textAlign: "center", marginBottom: "1rem" }}
>
Personal Details
</Typography>
<Typography variant="h5" gutterBottom component="div">
{name}
</Typography>
<Typography variant="h6" gutterBottom component="div">
{phone}
</Typography>
</CardContent>
</Card>
<Card sx={{ minWidth: 250, marginBottom: "2rem" }}>
<CardContent>
<Typography
variant="body"
gutterBottom
component="div"
sx={{ textAlign: "center", marginBottom: "1rem" }}
>
Professional Details
</Typography>
<Typography variant="h6" gutterBottom component="div">
{company}
</Typography>
<Typography variant="h6" gutterBottom component="div">
{email}
</Typography>
</CardContent>
</Card>
<Card sx={{ minWidth: 250 }}>
<CardContent>
<Typography
variant="body"
gutterBottom
component="div"
sx={{ textAlign: "center", marginBottom: "1rem" }}
>
Labels
</Typography>
<Fab variant="extended" size="small">
{labels}
</Fab>
</CardContent>
</Card>
</>
);
};
export default DetailsCard;
src/components/DetailsCard/DetailsCard.js
The end result will look like this
Much better now!
Before we can proceed with Notes, we need to get our backend ready. As we will be storing the notes in database for data persistence. Thankfully, creating a backend on Canonic is a breeze
Time to get your APIs ready!
Getting your APIs for CRM app comes at real ease. All you have to do is clone this production-ready project on Canonic and you're done. It will provide you with the backend, APIs, and documentation you need for integration, without writing any code. You will need to create entries for customers on CMS, you can simply copy the contents from customer
statics array we created.
Backend integration with GraphQL
Let's now integrate! Now that we have our APIs ready, let's move on by installing GraphQL packages.
- Install GraphQL packages
To pull our data from the backend, we will need two packages - Apollo Client and GraphQL
npm i @apollo/client graphql
- Configure GraphQL to communicate with backend
Configure the Apollo Client in the project directory, inside index.js
configure your apollo client so it would communicate with the backend.
Note to replace the
uri
with the one you'll get from Canonic.
import React from "react";
import ReactDOM from "react-dom";
import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client";
import ContentTable from "./components/ContentTable";
import "./index.css";
const client = new ApolloClient({
uri: "https://crm-app.can.canonic.dev/graphql",
cache: new InMemoryCache(),
});
ReactDOM.render(
<React.StrictMode>
<ApolloProvider client={client}>
<ContentTable />
</ApolloProvider>
</React.StrictMode>,
document.getElementById("root")
);
src/index.js
- Query the data
For querying the data we will create a directory inside src called gql and create a file inside it called query.js. In it will write all the data we need from the backend.
import { gql } from "@apollo/client";
export const GET_CUSTOMERS = gql`
query {
customers {
name
_id
createdAt
updatedAt
email
phone
company
labels
notes {
description
}
}
}
`;
src/gql/query.js
- Propagate the API’s data.
Here we are propagating the data to ContentTable component. All we have to do is replace the customer
static array we had with the code we get from API and replace all the references made to the static array with dynamic data.
import { React, useState } from "react";
import { useQuery } from "@apollo/client";
import {
Table,
TableBody,
TableContainer,
TableFooter,
TablePagination,
TableRow,
Paper,
TableCell,
TableHead,
CircularProgress,
} from "@mui/material";
import { GET_CUSTOMERS } from "../../gql/query";
import { TablePaginationActions } from "../Pagination";
import Modals from "../Modal";
export default function ContentTable() {
const { data, loading } = useQuery(GET_CUSTOMERS);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
const [customerData, setCustomerData] = useState();
const [isOpen, setIsOpen] = useState();
const emptyRows =
page > 0
? Math.max(0, (1 + page) * rowsPerPage - data.customers.length)
: 0;
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
const handleClick = (data) => {
setIsOpen(true);
setCustomerData(data);
};
return (
<>
{loading && (
<CircularProgress
sx={{ position: "absolute", top: "50%", left: "50%" }}
/>
)}
{!loading && (
<TableContainer component={Paper} sx={{ margin: "2rem", width: "95%" }}>
<Table sx={{ minWidth: 500 }} aria-label="custom pagination table">
<TableHead>
<TableRow>
<TableCell
align="left"
sx={{
backgroundColor: "black",
color: "white",
borderRight: "1px solid white",
}}
>
Name
</TableCell>
<TableCell
align="left"
sx={{
backgroundColor: "black",
color: "white",
borderRight: "1px solid white",
}}
>
Company
</TableCell>
<TableCell
align="left"
sx={{
backgroundColor: "black",
color: "white",
borderRight: "1px solid white",
}}
>
Email
</TableCell>
<TableCell
align="left"
sx={{
backgroundColor: "black",
color: "white",
borderRight: "1px solid white",
}}
>
Phone
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data &&
(rowsPerPage > 0
? data?.customers?.slice(
page * rowsPerPage,
page * rowsPerPage + rowsPerPage
)
: data.customers
).map((row, index) => (
<TableRow
key={index}
onClick={() => {
handleClick(row);
}}
sx={{ cursor: "pointer" }}
>
<TableCell
component="th"
scope="row"
sx={{ width: 160, borderRight: "1px solid black" }}
>
{row.name}
</TableCell>
<TableCell
sx={{ width: 160, borderRight: "1px solid black" }}
align="left"
>
{row.company}
</TableCell>
<TableCell
sx={{ width: 160, borderRight: "1px solid black" }}
align="left"
>
{row.email}
</TableCell>
<TableCell sx={{ width: 160 }} align="left">
{row.phone}
</TableCell>
</TableRow>
))}
{emptyRows > 0 && (
<TableRow style={{ height: 53 * emptyRows }}>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25, { label: "All", value: -1 }]}
colSpan={3}
count={data?.customers?.length}
rowsPerPage={rowsPerPage}
page={page}
SelectProps={{
inputProps: {
"aria-label": "rows per page",
},
native: true,
}}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
ActionsComponent={TablePaginationActions}
/>
</TableRow>
</TableFooter>
</Table>
<Modals data={customerData} open={isOpen} setIsOpen={setIsOpen} />
</TableContainer>
)}
</>
);
}
src/components/ContentTable/ContentTable.js
That’s all the changes we needed to integrate Canonic’s backend. Let get on to creating the Notes component.
- Create Notes component
Before we create Notes component, let’s first prepare Modal component to display notes.
We will be doing these 3 things in Modal component
- Creating a state to hold all our notes
- Create a state to hold status on opening add a new note text box
- Create a button that can switch between Displaying the notes to adding a note
We will faux import Notes component before it is created and feed in the data so when we finally create the component, we will already have a data to work with it
import Notes from "../Notes";
const [openAdd, setOpenAdd] = useState(false);
const [displayNotes, setDisplayNotes] = useState();
useEffect(() => {
data && setDisplayNotes(data.notes);
}, [data]);
<Item sx={{ backgroundColor: "inherit", marginTop: "2rem" }}>
{!openAdd && (
<Box sx={{ flexGrow: 1 }}>
<Paper
elevation={1}
sx={{
padding: "2rem",
height: "30rem",
overflow: "auto",
}}
>
{displayNotes?.map((note) => (
<List>
<ListItem sx={{ borderBottom: "1px solid black" }}>
<ListItemText>{note.description}</ListItemText>
</ListItem>
</List>
))}
{!openAdd && (
<Button
variant="outlined"
startIcon={<AddIcon />}
sx={{ marginTop: "1rem" }}
onClick={() => setOpenAdd(true)}
color="success"
>
Add a note
</Button>
)}
</Paper>
</Box>
)}
{openAdd && (
<Notes
setOpenAdd={setOpenAdd}
_id={data._id}
noteDescription={data?.notes?.map((item) => ({
description: item.description,
}))}
setDisplayNotes={setDisplayNotes}
/>
)}
After this, the Modal component shall look like this
import React, { useState, useEffect } from "react";
import { styled } from "@mui/material/styles";
import {
Typography,
Fade,
Modal,
Grid,
Paper,
Backdrop,
Box,
Button,
List,
ListItem,
ListItemText,
} from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import DetailsCard from "../DetailsCard";
import Notes from "../Notes";
const Item = styled(Paper)(({ theme }) => ({
...theme.typography.body2,
padding: theme.spacing(1),
textAlign: "center",
color: theme.palette.text.secondary,
boxShadow: "none",
}));
export default function Modals({ data, open, setIsOpen }) {
const [openAdd, setOpenAdd] = useState(false);
const [displayNotes, setDisplayNotes] = useState();
const handleClose = () => {
setIsOpen(!open);
};
useEffect(() => {
data && setDisplayNotes(data.notes);
}, [data]);
return (
<div>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={open}
onClose={handleClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<Fade in={open}>
<Grid
container
spacing={1}
sx={{
width: "95%",
height: "95%",
backgroundColor: "white",
position: "absolute",
top: "4%",
left: "3%",
}}
>
<Grid item xs={3} sx={{ padding: "5px" }}>
<DetailsCard
name={data.name}
company={data.company}
email={data.email}
phone={data.phone}
labels={data.labels}
/>
</Grid>
<Grid
item
xs={6}
sx={{ backgroundColor: "lightgray", padding: "5px" }}
>
<Item sx={{ backgroundColor: "inherit", marginTop: "2rem" }}>
<Typography variant="h6" gutterBottom component="div">
Notes
</Typography>
{!openAdd && (
<Box sx={{ flexGrow: 1 }}>
<Paper
elevation={1}
sx={{
padding: "2rem",
height: "30rem",
overflow: "auto",
}}
>
{displayNotes?.map((note) => (
<List>
<ListItem sx={{ borderBottom: "1px solid black" }}>
<ListItemText>{note.description}</ListItemText>
</ListItem>
</List>
))}
{!openAdd && (
<Button
variant="outlined"
startIcon={<AddIcon />}
sx={{ marginTop: "1rem" }}
onClick={() => setOpenAdd(true)}
color="success"
>
Add a note
</Button>
)}
</Paper>
</Box>
)}
{openAdd && (
<Notes
setOpenAdd={setOpenAdd}
_id={data._id}
noteDescription={data?.notes?.map((item) => ({
description: item.description,
}))}
setDisplayNotes={setDisplayNotes}
/>
)}
</Item>
</Grid>
<Grid item xs={3} sx={{ padding: "5px", marginTop: "2rem" }}>
<Item>
<Typography variant="h6" gutterBottom component="div">
Deals
</Typography>
<Typography variant="p" gutterBottom component="div">
Track the revenue opportunities associated with this record
</Typography>
</Item>
<Item sx={{ marginTop: "2rem" }}>
<Typography variant="h6" gutterBottom component="div">
Tickets
</Typography>
<Typography variant="p" gutterBottom component="div">
Track the customer requests associated with this record
</Typography>
</Item>
</Grid>
</Grid>
</Fade>
</Modal>
</div>
);
}
src/components/Modal/Modal.js
We just now need to add the Mutation, that way we can send the new notes to our API
Let’s go back to GQL directory and create a file called Mutation.js
import { gql } from "@apollo/client";
export const ADD_NOTE = gql`
mutation updateCustomerMutation($_id: ID!, $notes: [CustomerNoteInput!]!) {
updateCustomer(_id: $_id, input: { notes: $notes }) {
_id
createdAt
updatedAt
name
email
phone
company
notes {
description
}
}
}
`;
src/gql/mutation.js
This mutations just updates the database with a new note whenever it is created.
Now we have Modal component conditioned, let’s begin creating Notes component. Create a directory inside components and name it Notes, create a file inside of it and call it Notes.js
import { useRef } from "react";
import { useMutation } from "@apollo/client";
import { Box, TextField, Grid, Paper, Stack, Button } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
import SaveIcon from "@mui/icons-material/Save";
import { ADD_NOTE } from "../../gql/mutation";
const Notes = ({ _id, noteDescription, setOpenAdd, setDisplayNotes }) => {
const descriptionRef = useRef();
const [addNote] = useMutation(ADD_NOTE);
const handleClick = () => {
let description = descriptionRef.current.value;
if (description.replace(/\s+/g, " ").length) {
addNote({
variables: {
_id,
notes: [...noteDescription, { description }],
},
});
setDisplayNotes((oldList) => [...oldList, { description }]);
setOpenAdd(false);
}
};
const handleClose = () => {
setOpenAdd(false);
};
return (
<Box sx={{ flexGrow: 1 }}>
<Paper elevation={6} sx={{ padding: "2rem" }}>
<Stack spacing={2}>
<Grid container sx={{ marginTop: "1rem" }}>
<Grid item xs={12}>
<TextField
id="outlined-basic"
label="Description"
variant="outlined"
multiline
rows={2}
sx={{ width: "100%" }}
inputRef={descriptionRef}
/>
</Grid>
</Grid>
<Grid sx={{ display: "flex", justifyContent: "space-between" }}>
<LoadingButton
loadingPosition="start"
startIcon={<SaveIcon />}
variant="outlined"
sx={{ width: "25%" }}
onClick={handleClick}
color="success"
>
Save
</LoadingButton>
<Button
variant="outlined"
color="error"
sx={{ width: "25%" }}
onClick={handleClose}
>
Close
</Button>
</Grid>
</Stack>
</Paper>
</Box>
);
};
export default Notes;
src/components/Notes/Notes.js
In here while updating the database with mutation, we are also updating displayNotes
state, so the component would refresh and new note will be visible immediately
The end result should look like this
- Create Deals componenet.
We are going to have a drop here where the user could set any deal to the customer, the data will get updated the database.
Let’s begin by defining a mutation for it. Navigate back to mutation.js that resides in gql directory.
import { gql } from "@apollo/client";
export const ADD_NOTE = gql`
mutation updateCustomerMutation($_id: ID!, $notes: [CustomerNoteInput!]!) {
updateCustomer(_id: $_id, input: { notes: $notes }) {
_id
createdAt
updatedAt
name
email
phone
company
notes {
description
}
}
}
`;
export const ADD_DEAL = gql`
mutation updateDealsMutation($_id: ID!, $deals: [ID!]!) {
updateDeal(_id: $_id, input: { deals: $deals }) {
title
amount
dealOwner
deals {
_id
name
company
phone
email
}
}
}
`;
src/gql/mutation.js
We don’t need much from Modal in this case so let begin by creating Deals component, create a directory inside components name it Deals, create a file inside it and name it Deals.js
The flow is pretty much same as Notes in here, We are expecting a _id
prop from Modal here.
Lets pass it
import React, { useState } from "react";
import { styled } from "@mui/material/styles";
import {
Typography,
Fade,
Modal,
Grid,
Paper,
Backdrop,
Box,
Button,
List,
ListItem,
ListItemText,
} from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import DetailsCard from "../DetailsCard";
import Notes from "../Notes";
import Deals from "../Deals";
const Item = styled(Paper)(({ theme }) => ({
...theme.typography.body2,
padding: theme.spacing(1),
textAlign: "center",
color: theme.palette.text.secondary,
boxShadow: "none",
}));
export default function Modals({ data, open, setIsOpen }) {
const [openAdd, setOpenAdd] = useState(false);
const [displayNotes, setDisplayNotes] = useState();
const handleClose = () => {
setIsOpen(!open);
};
return (
<div>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={open}
onClose={handleClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<Fade in={open}>
<Grid
container
spacing={1}
sx={{
width: "95%",
height: "95%",
backgroundColor: "white",
position: "absolute",
top: "4%",
left: "3%",
}}
>
<Grid item xs={3} sx={{ padding: "5px" }}>
<DetailsCard
name={data.name}
company={data.company}
email={data.email}
phone={data.phone}
labels={data.labels}
/>
</Grid>
<Grid
item
xs={6}
sx={{ backgroundColor: "lightgray", padding: "5px" }}
>
<Item sx={{ backgroundColor: "inherit", marginTop: "2rem" }}>
<Typography variant="h6" gutterBottom component="div">
Notes
</Typography>
{!openAdd && (
<Box sx={{ flexGrow: 1 }}>
<Paper
elevation={1}
sx={{
padding: "2rem",
height: "30rem",
overflow: "auto",
}}
>
{displayNotes?.map((note) => (
<List>
<ListItem sx={{ borderBottom: "1px solid black" }}>
<ListItemText>{note.description}</ListItemText>
</ListItem>
</List>
))}
{!openAdd && (
<Button
variant="outlined"
startIcon={<AddIcon />}
sx={{ marginTop: "1rem" }}
onClick={() => setOpenAdd(true)}
color="success"
>
Add a note
</Button>
)}
</Paper>
</Box>
)}
{openAdd && (
<Notes
setOpenAdd={setOpenAdd}
_id={data._id}
noteDescription={data?.notes?.map((item) => ({
description: item.description,
}))}
setDisplayNotes={setDisplayNotes}
/>
)}
</Item>
</Grid>
<Grid item xs={3} sx={{ padding: "5px", marginTop: "2rem" }}>
<Item>
<Typography variant="h6" gutterBottom component="div">
Deals
</Typography>
<Typography variant="p" gutterBottom component="div">
Track the revenue opportunities associated with this record
</Typography>
<Deals userId={data._id} />
</Item>
<Item sx={{ marginTop: "2rem" }}>
<Typography variant="h6" gutterBottom component="div">
Tickets
</Typography>
<Typography variant="p" gutterBottom component="div">
Track the customer requests associated with this record
</Typography>
</Item>
</Grid>
</Grid>
</Fade>
</Modal>
</div>
);
}
src/components/Modal/Modal.js
And, our Deals component is done. It should look like this
- Create a Tickets component
This component will make a POST request to Asana API whenever a user inputs a title for the ticket, we are using create a task API for this you can read more about it here
It requires Title
and either workspace
or projects
or parent
defined in order to create a task
import { useRef, useState } from "react";
import { Box, TextField, Grid, Paper, Stack, Button } from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import DoneIcon from "@mui/icons-material/Done";
const Tickets = () => {
const [addTicket, setAddTicket] = useState(false);
const [addedTicketStatus, setAddedTicketStatus] = useState(false);
const ticketNameRef = useRef();
const handleAdd = () => {
let data = {
data: {
name: ticketNameRef.current.value,
workspace: process.env.REACT_APP_ASANA_WORKSPACE,
},
};
if (!addedTicketStatus)
fetch("https://app.asana.com/api/1.0/tasks", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: process.env.REACT_APP_ASANA_KEY,
},
body: JSON.stringify(data),
})
.then((res) => res.json())
.then(() => setAddedTicketStatus(true));
};
const handleClose = () => {
setAddTicket(false);
setAddedTicketStatus(false);
};
return (
<Box sx={{ flexGrow: 1 }}>
{!addTicket && (
<Button
variant="outlined"
color="success"
onClick={() => setAddTicket(true)}
>
Add a ticket to Asana
</Button>
)}
{addTicket && (
<Paper elevation={6} sx={{ padding: "2rem" }}>
<Stack spacing={2}>
<Grid container sx={{ marginTop: "1rem" }}>
<Grid item xs={12}>
<TextField
id="outlined-basic"
label="Ticket name"
variant="outlined"
sx={{ width: "100%" }}
inputRef={ticketNameRef}
/>
</Grid>
</Grid>
<Grid sx={{ display: "flex", justifyContent: "space-between" }}>
<Button
startIcon={addedTicketStatus ? <DoneIcon /> : <AddIcon />}
variant="outlined"
color="success"
onClick={handleAdd}
disabled={addedTicketStatus}
>
{addedTicketStatus ? "Added" : "Add"}
</Button>
<Button variant="outlined" color="error" onClick={handleClose}>
Close
</Button>
</Grid>
</Stack>
</Paper>
)}
</Box>
);
};
export default Tickets;
src/components/Tickets/Tickets.js
We don’t need any props to be passed from Modal in Ticket component, so let’s just import it
import React, { useState } from "react";
import { styled } from "@mui/material/styles";
import {
Typography,
Fade,
Modal,
Grid,
Paper,
Backdrop,
Box,
Button,
List,
ListItem,
ListItemText,
} from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import DetailsCard from "../DetailsCard";
import Notes from "../Notes";
import Deals from "../Deals";
import Tickets from "../Tickets";
const Item = styled(Paper)(({ theme }) => ({
...theme.typography.body2,
padding: theme.spacing(1),
textAlign: "center",
color: theme.palette.text.secondary,
boxShadow: "none",
}));
export default function Modals({ data, open, setIsOpen }) {
const [openAdd, setOpenAdd] = useState(false);
const [displayNotes, setDisplayNotes] = useState();
const handleClose = () => {
setIsOpen(!open);
};
return (
<div>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={open}
onClose={handleClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<Fade in={open}>
<Grid
container
spacing={1}
sx={{
width: "95%",
height: "95%",
backgroundColor: "white",
position: "absolute",
top: "4%",
left: "3%",
}}
>
<Grid item xs={3} sx={{ padding: "5px" }}>
<DetailsCard
name={data.name}
company={data.company}
email={data.email}
phone={data.phone}
labels={data.labels}
/>
</Grid>
<Grid
item
xs={6}
sx={{ backgroundColor: "lightgray", padding: "5px" }}
>
<Item sx={{ backgroundColor: "inherit", marginTop: "2rem" }}>
<Typography variant="h6" gutterBottom component="div">
Notes
</Typography>
{!openAdd && (
<Box sx={{ flexGrow: 1 }}>
<Paper
elevation={1}
sx={{
padding: "2rem",
height: "30rem",
overflow: "auto",
}}
>
{displayNotes?.map((note) => (
<List>
<ListItem sx={{ borderBottom: "1px solid black" }}>
<ListItemText>{note.description}</ListItemText>
</ListItem>
</List>
))}
{!openAdd && (
<Button
variant="outlined"
startIcon={<AddIcon />}
sx={{ marginTop: "1rem" }}
onClick={() => setOpenAdd(true)}
color="success"
>
Add a note
</Button>
)}
</Paper>
</Box>
)}
{openAdd && (
<Notes
setOpenAdd={setOpenAdd}
_id={data._id}
noteDescription={data?.notes?.map((item) => ({
description: item.description,
}))}
setDisplayNotes={setDisplayNotes}
/>
)}
</Item>
</Grid>
<Grid item xs={3} sx={{ padding: "5px", marginTop: "2rem" }}>
<Item>
<Typography variant="h6" gutterBottom component="div">
Deals
</Typography>
<Typography variant="p" gutterBottom component="div">
Track the revenue opportunities associated with this record
</Typography>
<Deals userId={data._id} />
</Item>
<Item sx={{ marginTop: "2rem" }}>
<Typography variant="h6" gutterBottom component="div">
Tickets
</Typography>
<Typography variant="p" gutterBottom component="div">
Track the customer requests associated with this record
</Typography>
<Tickets />
</Item>
</Grid>
</Grid>
</Fade>
</Modal>
</div>
);
}
src/component/Modal/Modal.js
Now we have Tickets component ready, it should look like this
- Create a Header component.
This will be a simple App bar that will display the a bar at the top of the page with the title of our project.
Let’s create a directory inside components and name it Header and a create file inside it and name Header.js
import React from "react";
import { AppBar, Box, Toolbar, Typography } from "@mui/material";
const Header = () => {
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static" sx={{ backgroundColor: "#000000" }}>
<Toolbar>
<Typography
variant="h6"
noWrap
component="div"
sx={{
flexGrow: 1,
display: { xs: "none", sm: "block" },
textAlign: "center",
}}
>
CRM
</Typography>
</Toolbar>
</AppBar>
</Box>
);
};
export default Header;
src/components/Header/Header.js
All we have to do now is to import it in index.js file
import React from "react";
import ReactDOM from "react-dom";
import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client";
import ContentTable from "./components/ContentTable";
import Header from "./components/Header";
import "./index.css";
const client = new ApolloClient({
uri: "https://crm-app.can.canonic.dev/graphql",
cache: new InMemoryCache(),
});
ReactDOM.render(
<React.StrictMode>
<ApolloProvider client={client}>
<Header />
<ContentTable />
</ApolloProvider>
</React.StrictMode>,
document.getElementById("root")
);
src/index.js
Voila! we are done.
BONUS
You can create log all the customer deals on Google Sheet.
Canonic has Google Sheet’s integrations, so all you have to do is open your cloned project, navigate to API, there select Deals table, Click on DB Triggers, there you will find a predefined Database trigger called UpdateTheSheet
Click on Google Sheets webhook. You will have to authenticate yourself with a Google account. Then go to required
and change the SpreadsheetID with your spreadsheet, make sure it is associated with the Google account you authenticated yourself with. You can change the range if you want. And, that’s it, you have successfully integrated Google Sheets into your CRM app now whenever user create a deal for a customer, DB trigger will update the Google Sheet.
Conclusion: It was a long journey well, I hope it was worth it. At the end you you learned how to created a feature packed awesome app, If you want, you can also duplicate this project from Canonic's sample app and easily get started by customizing it as per your experience. Check it out app.canonic.dev.
You can also check out our other guides here.
Join us on discord to discuss or share with our community. Write to us for any support requests at support@canonic.dev. Check out our website to know more about Canonic.
Top comments (3)
check idurar (github.com/idurar/idurar-erp-crm) Open Source ERP/CRM Based on Mern Stack (Node.js / Express.js / MongoDb / React.js ) with Ant Design (AntD) and Redux
HII Sir
I liked your ERP project very much, your project is worthy of praise and I thank you wholeheartedly for it. I am a beginning coder. Can you write a blog post on how to write this code step by step if it is helpful to you? By doing this it will be very convenient for the coder to build logic.
Your admirer.
hi EDIE,
I loved your projects. basically i'm getting an error from this project could you please help me to find the solution.
{
"success": false,
"message": "Api url doesn't exist "
}