Finished contact CRUD and frontend with websockets

This commit is contained in:
canove
2020-07-24 10:57:16 -03:00
parent 6d4e2a60a9
commit e266765caa
11 changed files with 343 additions and 136 deletions

View File

@@ -61,14 +61,18 @@ exports.store = async (req, res) => {
exports.show = async (req, res) => {
const { contactId } = req.params;
const { id, name, number, extraInfo } = await Contact.findByPk(contactId, {
include: [{ model: ContactCustomField, as: "extraInfo" }],
});
const { id, name, number, email, extraInfo } = await Contact.findByPk(
contactId,
{
include: [{ model: ContactCustomField, as: "extraInfo" }],
}
);
res.status(200).json({
id,
name,
number,
email,
extraInfo,
});
};
@@ -93,3 +97,23 @@ exports.update = async (req, res) => {
res.status(200).json(contact);
};
exports.delete = async (req, res) => {
const io = getIO();
const { contactId } = req.params;
const contact = await Contact.findByPk(contactId);
if (!contact) {
return res.status(400).json({ error: "No contact found with this ID" });
}
await contact.destroy();
io.emit("contact", {
action: "delete",
contactId: contactId,
});
res.status(200).json({ message: "Contact deleted" });
};

View File

@@ -13,4 +13,6 @@ routes.post("/contacts", isAuth, ContactController.store);
routes.put("/contacts/:contactId", isAuth, ContactController.update);
routes.delete("/contacts/:contactId", isAuth, ContactController.delete);
module.exports = routes;

View File

@@ -22,7 +22,6 @@
"react-modal-image": "^2.5.0",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"shortid": "^2.2.15",
"socket.io-client": "^2.3.0"
},
"scripts": {

View File

@@ -0,0 +1,43 @@
import React from "react";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import Typography from "@material-ui/core/Typography";
const ConfirmationModal = ({ title, children, open, setOpen, onConfirm }) => {
return (
<Dialog
open={open}
onClose={() => setOpen(false)}
aria-labelledby="confirm-dialog"
>
<DialogTitle id="confirm-dialog">{title}</DialogTitle>
<DialogContent dividers>
<Typography>{children}</Typography>
</DialogContent>
<DialogActions>
<Button
variant="contained"
onClick={() => setOpen(false)}
color="secondary"
>
Cancelar
</Button>
<Button
variant="contained"
onClick={() => {
setOpen(false);
onConfirm();
}}
color="default"
>
Confirmar
</Button>
</DialogActions>
</Dialog>
);
};
export default ConfirmationModal;

View File

@@ -1,37 +0,0 @@
import React from "react";
import { Navbar, Nav, Container } from "react-bootstrap";
import { LinkContainer } from "react-router-bootstrap";
const LogedinNavbar = () => {
return (
<div>
<Navbar variant="dark" bg="dark" expand="lg">
<Container>
<LinkContainer to="/" style={{ color: "#519032" }}>
<Navbar.Brand>EconoWhatsBot</Navbar.Brand>
</LinkContainer>
<Navbar.Toggle aria-controls="responsive-navbar-nav" />
<Navbar.Collapse id="responsive-navbar-nav">
<Nav className="mr-auto">
<LinkContainer to="/">
<Nav.Link href="#home">Home</Nav.Link>
</LinkContainer>
<LinkContainer to="/chat">
<Nav.Link href="#link">Chat</Nav.Link>
</LinkContainer>
</Nav>
<LinkContainer to="/login">
<Nav.Link href="#login">Login</Nav.Link>
</LinkContainer>
<LinkContainer to="/signup">
<Nav.Link href="#signup">Signup</Nav.Link>
</LinkContainer>
</Navbar.Collapse>
</Container>
</Navbar>
</div>
);
};
export default LogedinNavbar;

View File

@@ -1,52 +0,0 @@
import React from "react";
import { useHistory } from "react-router-dom";
import { Navbar, Nav, Container } from "react-bootstrap";
import { LinkContainer } from "react-router-bootstrap";
import "./Navbar.css";
const DefaultNavbar = () => {
const username = localStorage.getItem("username");
const history = useHistory();
const handleLogout = e => {
e.preventDefault();
localStorage.removeItem("token");
localStorage.removeItem("userName");
localStorage.removeItem("userId");
history.push("/");
};
return (
<div>
<Navbar variant="dark" bg="dark" expand="lg">
<Container>
<LinkContainer to="/" style={{ color: "#519032" }}>
<Navbar.Brand>EconoWhatsBot</Navbar.Brand>
</LinkContainer>
<Navbar.Toggle aria-controls="responsive-navbar-nav" />
<Navbar.Collapse id="responsive-navbar-nav">
<Nav className="mr-auto">
<LinkContainer to="/">
<Nav.Link href="#home">Home</Nav.Link>
</LinkContainer>
<LinkContainer to="/chat">
<Nav.Link href="#link">Chat</Nav.Link>
</LinkContainer>
<LinkContainer to="/chat2">
<Nav.Link href="#link">Chat MaterialUi</Nav.Link>
</LinkContainer>
</Nav>
<Navbar.Text>
Logado como: <a href="#login">{username}</a>
</Navbar.Text>
<Nav.Link href="#logout" onClick={handleLogout}>
Logout
</Nav.Link>
</Navbar.Collapse>
</Container>
</Navbar>
</div>
);
};
export default DefaultNavbar;

View File

@@ -12,6 +12,8 @@ import DialogTitle from "@material-ui/core/DialogTitle";
import Typography from "@material-ui/core/Typography";
import IconButton from "@material-ui/core/IconButton";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import CircularProgress from "@material-ui/core/CircularProgress";
import { green } from "@material-ui/core/colors";
import { makeStyles } from "@material-ui/core/styles";
@@ -34,6 +36,20 @@ const useStyles = makeStyles(theme => ({
justifyContent: "center",
alignItems: "center",
},
btnWrapper: {
// margin: theme.spacing(1),
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
}));
const ContactModal = ({
@@ -82,17 +98,12 @@ const ContactModal = ({
handleClose();
};
console.log(contact);
console.log("id", contactId);
return (
<div className={classes.root}>
<Dialog
open={modalOpen}
onClose={handleClose}
aria-labelledby="form-dialog-title"
maxWidth="lg"
// maxHeight="xs"
scroll="paper"
className={classes.modal}
>
@@ -117,7 +128,7 @@ const ContactModal = ({
}) => (
<>
<DialogTitle id="form-dialog-title">
Adicionar contato
{contactId ? "Editar contato" : "Adicionar contato"}
</DialogTitle>
<DialogContent dividers>
<Typography variant="subtitle1" gutterBottom>
@@ -215,11 +226,28 @@ const ContactModal = ({
</FieldArray>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="secondary">
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
Cancelar
</Button>
<Button onClick={handleSubmit} color="primary">
Adicionar
<Button
onClick={handleSubmit}
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
{contactId ? "Salvar" : "Adicionar"}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</DialogActions>
</>

View File

@@ -25,6 +25,9 @@ import EditIcon from "@material-ui/icons/Edit";
import PaginationActions from "./PaginationActions";
import api from "../../util/api";
import ContactModal from "./ContactModal";
import ContactsSekeleton from "./ContactsSekeleton";
import ConfirmationModal from "../../components/ConfirmationModal/ConfirmationModal";
const useStyles = makeStyles(theme => ({
mainContainer: {
@@ -68,7 +71,7 @@ const Contacts = () => {
const classes = useStyles();
const token = localStorage.getItem("token");
const userId = localStorage.getItem("userId");
// const userId = localStorage.getItem("userId");
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(0);
@@ -80,6 +83,9 @@ const Contacts = () => {
const [modalOpen, setModalOpen] = useState(false);
const [deletingContact, setDeletingContact] = useState(null);
const [confirmOpen, setConfirmOpen] = useState(false);
useEffect(() => {
setLoading(true);
const delayDebounceFn = setTimeout(() => {
@@ -108,6 +114,10 @@ const Contacts = () => {
if (data.action === "update" || data.action === "create") {
updateContacts(data.contact);
}
if (data.action === "delete") {
deleteContact(data.contactId);
}
});
return () => {
@@ -128,6 +138,18 @@ const Contacts = () => {
});
};
const deleteContact = contactId => {
setContacts(prevState => {
const contactIndex = prevState.findIndex(c => c.id === +contactId);
if (contactIndex === -1) return prevState;
const aux = [...prevState];
aux.splice(contactIndex, 1);
return aux;
});
};
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
@@ -156,6 +178,11 @@ const Contacts = () => {
setModalOpen(true);
};
const handleDeleteContact = async contactId => {
await api.delete(`/contacts/${contactId}`);
setDeletingContact(null);
};
return (
<Container className={classes.mainContainer}>
<ContactModal
@@ -165,6 +192,15 @@ const Contacts = () => {
aria-labelledby="form-dialog-title"
contactId={selectedContactId}
></ContactModal>
<ConfirmationModal
title={deletingContact && `Deletar ${deletingContact.name}`}
open={confirmOpen}
setOpen={setConfirmOpen}
onConfirm={e => handleDeleteContact(deletingContact.id)}
>
Tem certeza que deseja deletar este contato? Todos os tickets
relacionados serão perdidos.
</ConfirmationModal>
<div className={classes.contactsHeader}>
<Typography variant="h5" gutterBottom>
Contatos
@@ -204,27 +240,40 @@ const Contacts = () => {
</TableRow>
</TableHead>
<TableBody>
{contacts.map(contact => (
<TableRow key={contact.id}>
<TableCell style={{ paddingRight: 0 }}>
{<Avatar src={contact.profilePicUrl} />}
</TableCell>
<TableCell>{contact.name}</TableCell>
<TableCell>{contact.number}</TableCell>
<TableCell>{contact.updatedAt}</TableCell>
<TableCell align="right">
<IconButton
size="small"
onClick={() => hadleEditContact(contact.id)}
>
<EditIcon />
</IconButton>
<IconButton size="small">
<DeleteOutlineIcon />
</IconButton>
</TableCell>
</TableRow>
))}
{loading ? (
<ContactsSekeleton />
) : (
<>
{contacts.map(contact => (
<TableRow key={contact.id}>
<TableCell style={{ paddingRight: 0 }}>
{<Avatar src={contact.profilePicUrl} />}
</TableCell>
<TableCell>{contact.name}</TableCell>
<TableCell>{contact.number}</TableCell>
<TableCell>{contact.email}</TableCell>
<TableCell align="right">
<IconButton
size="small"
onClick={() => hadleEditContact(contact.id)}
>
<EditIcon />
</IconButton>
<IconButton
size="small"
onClick={e => {
setConfirmOpen(true);
setDeletingContact(contact);
}}
>
<DeleteOutlineIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</>
)}
</TableBody>
<TableFooter>
<TableRow>

View File

@@ -0,0 +1,163 @@
import React from "react";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import Skeleton from "@material-ui/lab/Skeleton";
const ContactsSekeleton = () => {
return (
<>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={80} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={70} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={90} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={55} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={60} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={100} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={80} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={70} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={90} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={55} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={60} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={100} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={80} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={70} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={90} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={55} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={60} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={100} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={80} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={70} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={90} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={55} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={60} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={100} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={80} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={70} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={90} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingRight: 0 }}>
<Skeleton animation="wave" variant="circle" width={40} height={40} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={55} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={60} />
</TableCell>
<TableCell>
<Skeleton animation="wave" height={20} width={100} />
</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
</>
);
};
export default ContactsSekeleton;

View File

@@ -7372,11 +7372,6 @@ nan@^2.12.1:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
nanoid@^2.1.0:
version "2.1.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280"
integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==
nanomatch@^1.2.9:
version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@@ -9954,13 +9949,6 @@ shellwords@^0.1.1:
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
shortid@^2.2.15:
version "2.2.15"
resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.15.tgz#2b902eaa93a69b11120373cd42a1f1fe4437c122"
integrity sha512-5EaCy2mx2Jgc/Fdn9uuDuNIIfWBpzY4XIlhoqtXF6qsf+/+SGZ+FxDdX/ZsMZiWupIWNqAEmiNY4RC+LSmCeOw==
dependencies:
nanoid "^2.1.0"
side-channel@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947"