feat: start internatinalization

This commit is contained in:
canove
2020-08-12 19:55:41 -03:00
parent ecf5ea1eb3
commit 63732568e9
22 changed files with 356 additions and 11975 deletions

3
frontend/.gitignore vendored
View File

@@ -5,6 +5,7 @@
/.pnp /.pnp
.pnp.js .pnp.js
# testing # testing
/coverage /coverage
@@ -22,6 +23,6 @@
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
package-lock.json package-lock.json
yarn.lock yarn.lock

View File

@@ -13,6 +13,8 @@
"date-fns": "^2.14.0", "date-fns": "^2.14.0",
"emoji-mart": "^3.0.0", "emoji-mart": "^3.0.0",
"formik": "^2.1.5", "formik": "^2.1.5",
"i18next": "^19.6.3",
"i18next-browser-languagedetector": "^5.0.1",
"mic-recorder-to-mp3": "^2.2.1", "mic-recorder-to-mp3": "^2.2.1",
"qrcode.react": "^1.0.0", "qrcode.react": "^1.0.0",
"react": "^16.13.1", "react": "^16.13.1",

View File

@@ -2,10 +2,11 @@ import React, { useState, useEffect } from "react";
import { Formik, FieldArray } from "formik"; import { Formik, FieldArray } from "formik";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import Dialog from "@material-ui/core/Dialog"; import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions"; import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent"; import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle"; import DialogTitle from "@material-ui/core/DialogTitle";
@@ -13,9 +14,8 @@ import Typography from "@material-ui/core/Typography";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton";
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"; import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import CircularProgress from "@material-ui/core/CircularProgress"; import CircularProgress from "@material-ui/core/CircularProgress";
import { green } from "@material-ui/core/colors";
import { makeStyles } from "@material-ui/core/styles"; import { i18n } from "../../translate/i18n";
import api from "../../services/api"; import api from "../../services/api";
@@ -121,14 +121,16 @@ const ContactModal = ({ open, onClose, contactId }) => {
}) => ( }) => (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<DialogTitle id="form-dialog-title"> <DialogTitle id="form-dialog-title">
{contactId ? "Editar contato" : "Adicionar contato"} {contactId
? `${i18n.t("contactModal.title.edit")}`
: `${i18n.t("contactModal.title.add")}`}
</DialogTitle> </DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Typography variant="subtitle1" gutterBottom> <Typography variant="subtitle1" gutterBottom>
Dados do contato {i18n.t("contactModal.form.mainInfo")}
</Typography> </Typography>
<TextField <TextField
label="Nome" label={i18n.t("contactModal.form.name")}
name="name" name="name"
value={values.name || ""} value={values.name || ""}
onChange={handleChange} onChange={handleChange}
@@ -138,18 +140,18 @@ const ContactModal = ({ open, onClose, contactId }) => {
className={classes.textField} className={classes.textField}
/> />
<TextField <TextField
label="Número do Whatsapp" label={i18n.t("contactModal.form.number")}
name="number" name="number"
value={values.number || ""} value={values.number || ""}
onChange={handleChange} onChange={handleChange}
placeholder="Ex: 5513912344321" placeholder="5513912344321"
variant="outlined" variant="outlined"
margin="dense" margin="dense"
required required
/> />
<div> <div>
<TextField <TextField
label="Email" label={i18n.t("contactModal.form.email")}
name="email" name="email"
value={values.email || ""} value={values.email || ""}
onChange={handleChange} onChange={handleChange}
@@ -163,7 +165,7 @@ const ContactModal = ({ open, onClose, contactId }) => {
style={{ marginBottom: 8, marginTop: 12 }} style={{ marginBottom: 8, marginTop: 12 }}
variant="subtitle1" variant="subtitle1"
> >
Informações adicionais {i18n.t("contactModal.form.extraInfo")}
</Typography> </Typography>
<FieldArray name="extraInfo"> <FieldArray name="extraInfo">
@@ -177,7 +179,7 @@ const ContactModal = ({ open, onClose, contactId }) => {
key={`${index}-info`} key={`${index}-info`}
> >
<TextField <TextField
label="Nome do campo" label={i18n.t("contactModal.form.extraName")}
name={`extraInfo[${index}].name`} name={`extraInfo[${index}].name`}
value={info.name || ""} value={info.name || ""}
onChange={handleChange} onChange={handleChange}
@@ -187,7 +189,7 @@ const ContactModal = ({ open, onClose, contactId }) => {
className={classes.textField} className={classes.textField}
/> />
<TextField <TextField
label="Valor" label={i18n.t("contactModal.form.extraValue")}
name={`extraInfo[${index}].value`} name={`extraInfo[${index}].value`}
value={info.value || ""} value={info.value || ""}
onChange={handleChange} onChange={handleChange}
@@ -211,7 +213,7 @@ const ContactModal = ({ open, onClose, contactId }) => {
color="primary" color="primary"
onClick={() => push({ name: "", value: "" })} onClick={() => push({ name: "", value: "" })}
> >
+ Adicionar atributo {`+ ${i18n.t("contactModal.buttons.addExtraInfo")}`}
</Button> </Button>
</div> </div>
</> </>
@@ -225,7 +227,7 @@ const ContactModal = ({ open, onClose, contactId }) => {
disabled={isSubmitting} disabled={isSubmitting}
variant="outlined" variant="outlined"
> >
Cancelar {i18n.t("contactModal.buttons.cancel")}
</Button> </Button>
<Button <Button
type="submit" type="submit"
@@ -234,7 +236,9 @@ const ContactModal = ({ open, onClose, contactId }) => {
variant="contained" variant="contained"
className={classes.btnWrapper} className={classes.btnWrapper}
> >
{contactId ? "Salvar" : "Adicionar"} {contactId
? `${i18n.t("contactModal.buttons.okEdit")}`
: `${i18n.t("contactModal.buttons.okAdd")}`}
{isSubmitting && ( {isSubmitting && (
<CircularProgress <CircularProgress
size={24} size={24}

View File

@@ -1,28 +1,16 @@
import React from "react"; import React from "react";
import { Link as RouterLink } from "react-router-dom"; import { Link as RouterLink } from "react-router-dom";
import { makeStyles } from "@material-ui/core/styles";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem"; import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon"; import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText"; import ListItemText from "@material-ui/core/ListItemText";
// import ListSubheader from "@material-ui/core/ListSubheader";
import Collapse from "@material-ui/core/Collapse";
import DashboardIcon from "@material-ui/icons/Dashboard"; import DashboardIcon from "@material-ui/icons/Dashboard";
import WhatsAppIcon from "@material-ui/icons/WhatsApp"; import WhatsAppIcon from "@material-ui/icons/WhatsApp";
import SyncAltIcon from "@material-ui/icons/SyncAlt"; import SyncAltIcon from "@material-ui/icons/SyncAlt";
import ChatIcon from "@material-ui/icons/Chat";
import LayersIcon from "@material-ui/icons/Layers";
import ExpandLess from "@material-ui/icons/ExpandLess";
import ExpandMore from "@material-ui/icons/ExpandMore";
import ContactPhoneIcon from "@material-ui/icons/ContactPhone"; import ContactPhoneIcon from "@material-ui/icons/ContactPhone";
const useStyles = makeStyles(theme => ({ import { i18n } from "../../translate/i18n";
nested: {
paddingLeft: theme.spacing(4),
},
}));
function ListItemLink(props) { function ListItemLink(props) {
const { icon, primary, to, className } = props; const { icon, primary, to, className } = props;
@@ -46,76 +34,27 @@ function ListItemLink(props) {
} }
const MainListItems = () => { const MainListItems = () => {
const classes = useStyles();
const [open, setOpen] = React.useState(false);
const handleClick = () => {
setOpen(!open);
};
return ( return (
<div> <div>
<ListItemLink to="/" primary="Dashboard" icon={<DashboardIcon />} /> <ListItemLink to="/" primary="Dashboard" icon={<DashboardIcon />} />
<ListItem button onClick={handleClick}>
<ListItemIcon>
<WhatsAppIcon />
</ListItemIcon>
<ListItemText primary="WhatsApp" />
{open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
<ListItemLink <ListItemLink
className={classes.nested}
to="/whats-auth" to="/whats-auth"
primary="Conexão" primary={i18n.t("mainDrawer.listItems.connection")}
icon={<SyncAltIcon />} icon={<SyncAltIcon />}
/> />
<ListItemLink <ListItemLink
className={classes.nested}
to="/chat" to="/chat"
primary="Chat" primary={i18n.t("mainDrawer.listItems.tickets")}
icon={<ChatIcon />} icon={<WhatsAppIcon />}
/> />
</List>
</Collapse>
<ListItemLink <ListItemLink
to="/contacts" to="/contacts"
primary="Contatos" primary={i18n.t("mainDrawer.listItems.contacts")}
icon={<ContactPhoneIcon />} icon={<ContactPhoneIcon />}
/> />
<ListItem button disabled>
<ListItemIcon>
<LayersIcon />
</ListItemIcon>
<ListItemText primary="Integrações" />
</ListItem>
</div> </div>
); );
}; };
// export const secondaryListItems = (
// <div>
// <ListSubheader inset>Saved reports</ListSubheader>
// <ListItem button>
// <ListItemIcon>
// <AssignmentIcon />
// </ListItemIcon>
// <ListItemText primary="Current month" />
// </ListItem>
// <ListItem button>
// <ListItemIcon>
// <AssignmentIcon />
// </ListItemIcon>
// <ListItemText primary="Last quarter" />
// </ListItem>
// <ListItem button>
// <ListItemIcon>
// <AssignmentIcon />
// </ListItemIcon>
// <ListItemText primary="Year-end sale" />
// </ListItem>
// </div>
// );
export default MainListItems; export default MainListItems;

View File

@@ -221,8 +221,6 @@ const useStyles = makeStyles(theme => ({
}, },
})); }));
let socket;
const MessagesList = () => { const MessagesList = () => {
const { ticketId } = useParams(); const { ticketId } = useParams();
const history = useHistory(); const history = useHistory();
@@ -272,17 +270,9 @@ const MessagesList = () => {
}, [pageNumber, ticketId, history]); }, [pageNumber, ticketId, history]);
useEffect(() => { useEffect(() => {
socket = openSocket(process.env.REACT_APP_BACKEND_URL); const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
socket.emit("joinChatBox", ticketId, () => {}); socket.emit("joinChatBox", ticketId, () => {});
return () => {
socket.disconnect();
setPageNumber(1);
setMessagesList([]);
};
}, [ticketId]);
useEffect(() => {
socket.on("appMessage", data => { socket.on("appMessage", data => {
if (loading) return; if (loading) return;
@@ -300,7 +290,13 @@ const MessagesList = () => {
setContact(data.contact); setContact(data.contact);
} }
}); });
}, [loading]);
return () => {
socket.disconnect();
setPageNumber(1);
setMessagesList([]);
};
}, [ticketId, loading]);
const loadMore = () => { const loadMore = () => {
setPageNumber(prevPageNumber => prevPageNumber + 1); setPageNumber(prevPageNumber => prevPageNumber + 1);

View File

@@ -14,6 +14,7 @@ import { green } from "@material-ui/core/colors";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import { i18n } from "../../translate/i18n";
import api from "../../services/api"; import api from "../../services/api";
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
@@ -101,7 +102,9 @@ const NewTicketModal = ({ modalOpen, onClose, contactId }) => {
scroll="paper" scroll="paper"
> >
<form onSubmit={handleSaveTicket}> <form onSubmit={handleSaveTicket}>
<DialogTitle id="form-dialog-title">Criar Ticket</DialogTitle> <DialogTitle id="form-dialog-title">
{i18n.t("newTicketModal.title")}
</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Autocomplete <Autocomplete
id="asynchronous-demo" id="asynchronous-demo"
@@ -115,7 +118,7 @@ const NewTicketModal = ({ modalOpen, onClose, contactId }) => {
renderInput={params => ( renderInput={params => (
<TextField <TextField
{...params} {...params}
label="Digite para pesquisar o contato" label={i18n.t("newTicketModal.fieldLabel")}
variant="outlined" variant="outlined"
required required
autoFocus autoFocus
@@ -143,7 +146,7 @@ const NewTicketModal = ({ modalOpen, onClose, contactId }) => {
disabled={loading} disabled={loading}
variant="outlined" variant="outlined"
> >
Cancelar {i18n.t("newTicketModal.buttons.cancel")}
</Button> </Button>
<Button <Button
type="submit" type="submit"
@@ -152,7 +155,7 @@ const NewTicketModal = ({ modalOpen, onClose, contactId }) => {
variant="contained" variant="contained"
className={classes.btnWrapper} className={classes.btnWrapper}
> >
Salvar {i18n.t("newTicketModal.buttons.ok")}
{loading && ( {loading && (
<CircularProgress <CircularProgress
size={24} size={24}

View File

@@ -2,11 +2,13 @@ import React from "react";
import QRCode from "qrcode.react"; import QRCode from "qrcode.react";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import { i18n } from "../../translate/i18n";
const Qrcode = ({ qrCode }) => { const Qrcode = ({ qrCode }) => {
return ( return (
<div> <div>
<Typography color="primary" gutterBottom> <Typography color="primary" gutterBottom>
Leia o QrCode para iniciar a sessão {i18n.t("qrCode.message")}
</Typography> </Typography>
<QRCode value={qrCode} size={256} /> <QRCode value={qrCode} size={256} />
</div> </div>

View File

@@ -1,18 +1,20 @@
import React from "react"; import React from "react";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import { i18n } from "../../translate/i18n";
const SessionInfo = ({ session }) => { const SessionInfo = ({ session }) => {
console.log(session); console.log(session);
return ( return (
<div> <div>
<Typography component="h2" variant="h6" color="primary" gutterBottom> <Typography component="h2" variant="h6" color="primary" gutterBottom>
{`Status: ${session.status}`} {`${i18n.t("sessionInfo.status")}${session.status}`}
</Typography> </Typography>
<Typography component="p" variant="h6"> <Typography component="p" variant="h6">
{`Bateria: ${session.battery}%`} {`${i18n.t("sessionInfo.battery")}${session.battery}%`}
</Typography> </Typography>
<Typography color="textSecondary"> <Typography color="textSecondary">
{`Carregando: ${session.plugged} `} {`${i18n.t("sessionInfo.charging")}${session.plugged} `}
</Typography> </Typography>
</div> </div>
); );

View File

@@ -23,10 +23,9 @@ import NewTicketModal from "../NewTicketModal";
import TicketsList from "../TicketsList"; import TicketsList from "../TicketsList";
import TabPanel from "../TabPanel"; import TabPanel from "../TabPanel";
import { i18n } from "../../translate/i18n";
import api from "../../services/api"; import api from "../../services/api";
let socket;
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
ticketsWrapper: { ticketsWrapper: {
position: "relative", position: "relative",
@@ -209,15 +208,9 @@ const Tickets = () => {
}, [searchParam, pageNumber, token, tab]); }, [searchParam, pageNumber, token, tab]);
useEffect(() => { useEffect(() => {
socket = openSocket(process.env.REACT_APP_BACKEND_URL); const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
socket.emit("joinNotification"); socket.emit("joinNotification");
return () => {
socket.disconnect();
};
}, []);
useEffect(() => {
socket.on("ticket", data => { socket.on("ticket", data => {
if (loading) return; if (loading) return;
@@ -232,7 +225,7 @@ const Tickets = () => {
if (data.action === "delete") { if (data.action === "delete") {
deleteTicket(data); deleteTicket(data);
if (ticketId && data.ticketId === +ticketId) { if (ticketId && data.ticketId === +ticketId) {
toast.warn("O ticket que você estava foi deletado."); toast.warn(i18n.t("tickets.toasts.deleted"));
history.push("/chat"); history.push("/chat");
} }
} }
@@ -253,6 +246,10 @@ const Tickets = () => {
showDesktopNotification(data); showDesktopNotification(data);
} }
}); });
return () => {
socket.disconnect();
};
}, [history, ticketId, userId, loading]); }, [history, ticketId, userId, loading]);
const loadMore = () => { const loadMore = () => {
@@ -262,8 +259,6 @@ const Tickets = () => {
const updateTickets = ({ ticket }) => { const updateTickets = ({ ticket }) => {
setTickets(prevState => { setTickets(prevState => {
const ticketIndex = prevState.findIndex(t => t.id === ticket.id); const ticketIndex = prevState.findIndex(t => t.id === ticket.id);
if (prevState.length >= 20) {
if (ticketIndex !== -1) { if (ticketIndex !== -1) {
let aux = [...prevState]; let aux = [...prevState];
aux[ticketIndex] = ticket; aux[ticketIndex] = ticket;
@@ -272,9 +267,6 @@ const Tickets = () => {
} else { } else {
return [ticket, ...prevState]; return [ticket, ...prevState];
} }
} else {
return [ticket, ...prevState];
}
}); });
}; };
@@ -298,7 +290,10 @@ const Tickets = () => {
icon: contact.profilePicUrl, icon: contact.profilePicUrl,
tag: ticket.id, tag: ticket.id,
}; };
let notification = new Notification(`Mensagem de ${contact.name}`, options); let notification = new Notification(
`${i18n.t("tickets.notification.message")} ${contact.name}`,
options
);
notification.onclick = function (event) { notification.onclick = function (event) {
event.preventDefault(); // event.preventDefault(); //
@@ -396,19 +391,19 @@ const Tickets = () => {
<Tab <Tab
value={"open"} value={"open"}
icon={<MoveToInboxIcon />} icon={<MoveToInboxIcon />}
label="Inbox" label={i18n.t("tickets.tabs.open.title")}
classes={{ root: classes.tab }} classes={{ root: classes.tab }}
/> />
<Tab <Tab
value={"closed"} value={"closed"}
icon={<CheckBoxIcon />} icon={<CheckBoxIcon />}
label="Resolvidos" label={i18n.t("tickets.tabs.closed.title")}
classes={{ root: classes.tab }} classes={{ root: classes.tab }}
/> />
<Tab <Tab
value={"search"} value={"search"}
icon={<SearchIcon />} icon={<SearchIcon />}
label="Busca" label={i18n.t("tickets.tabs.search.title")}
classes={{ root: classes.tab }} classes={{ root: classes.tab }}
/> />
</Tabs> </Tabs>
@@ -418,7 +413,7 @@ const Tickets = () => {
<SearchIcon className={classes.searchIcon} /> <SearchIcon className={classes.searchIcon} />
<InputBase <InputBase
className={classes.contactsSearchInput} className={classes.contactsSearchInput}
placeholder="Pesquisar tickets e mensagens" placeholder={i18n.t("tickets.search.placeholder")}
type="search" type="search"
onChange={handleSearchContact} onChange={handleSearchContact}
/> />
@@ -432,19 +427,19 @@ const Tickets = () => {
onScroll={handleScroll} onScroll={handleScroll}
> >
<div className={classes.ticketsListHeader}> <div className={classes.ticketsListHeader}>
Atendendo {i18n.t("tickets.tabs.open.assignedHeader")}
<span className={classes.ticketsCount}> <span className={classes.ticketsCount}>
{countTickets("open", userId)} {countTickets("open", userId)}
</span> </span>
<div className={classes.ticketsListActions}> <div className={classes.ticketsListActions}>
<FormControlLabel <FormControlLabel
label="Todos" label={i18n.t("tickets.buttons.showAll")}
labelPlacement="start" labelPlacement="start"
control={ control={
<Switch <Switch
size="small" size="small"
checked={showAllTickets} checked={showAllTickets}
onChange={e => setShowAllTickets(prevState => !prevState)} onChange={() => setShowAllTickets(prevState => !prevState)}
name="showAllTickets" name="showAllTickets"
color="primary" color="primary"
/> />
@@ -467,8 +462,10 @@ const Tickets = () => {
showAllTickets={showAllTickets} showAllTickets={showAllTickets}
ticketId={ticketId} ticketId={ticketId}
handleAcepptTicket={handleAcepptTicket} handleAcepptTicket={handleAcepptTicket}
noTicketsTitle="Pronto pra mais?" noTicketsTitle={i18n.t("tickets.tabs.open.openNoTicketsTitle")}
noTicketsMessage="Aceite um ticket da fila para começar." noTicketsMessage={i18n.t(
"tickets.tabs.open.openNoTicketsMessage"
)}
status="open" status="open"
userId={userId} userId={userId}
/> />
@@ -482,7 +479,7 @@ const Tickets = () => {
onScroll={handleScroll} onScroll={handleScroll}
> >
<div className={classes.ticketsListHeader}> <div className={classes.ticketsListHeader}>
Aguardando {i18n.t("tickets.tabs.open.pendingHeader")}
<span className={classes.ticketsCount}> <span className={classes.ticketsCount}>
{countTickets("pending", null)} {countTickets("pending", null)}
</span> </span>
@@ -495,8 +492,10 @@ const Tickets = () => {
showAllTickets={showAllTickets} showAllTickets={showAllTickets}
ticketId={ticketId} ticketId={ticketId}
handleAcepptTicket={handleAcepptTicket} handleAcepptTicket={handleAcepptTicket}
noTicketsTitle="Tudo resolvido!" noTicketsTitle={i18n.t("tickets.tabs.open.pendingNoTicketsTitle")}
noTicketsMessage="Nenhum ticket pendente." noTicketsMessage={i18n.t(
"tickets.tabs.open.pendingNoTicketsMessage"
)}
status="pending" status="pending"
userId={null} userId={null}
/> />
@@ -541,8 +540,8 @@ const Tickets = () => {
showAllTickets={showAllTickets} showAllTickets={showAllTickets}
ticketId={ticketId} ticketId={ticketId}
handleAcepptTicket={handleAcepptTicket} handleAcepptTicket={handleAcepptTicket}
noTicketsTitle="Nada encontrado!" noTicketsTitle={i18n.t("tickets.tabs.search.noTicketsTitle")}
noTicketsMessage="Tente buscar por outro termo." noTicketsMessage={i18n.t("tickets.tabs.search.noTicketsMessage")}
status="all" status="all"
/> />
{loading && <TicketsSkeleton />} {loading && <TicketsSkeleton />}

View File

@@ -12,6 +12,8 @@ import Divider from "@material-ui/core/Divider";
import Badge from "@material-ui/core/Badge"; import Badge from "@material-ui/core/Badge";
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import { i18n } from "../../translate/i18n";
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
ticket: { ticket: {
position: "relative", position: "relative",
@@ -185,7 +187,7 @@ const TicketsList = ({
className="hidden-button" className="hidden-button"
onClick={e => handleAcepptTicket(ticket.id)} onClick={e => handleAcepptTicket(ticket.id)}
> >
Aceitar {i18n.t("ticketsList.buttons.accept")}
</Button> </Button>
) : null} ) : null}
</ListItem> </ListItem>

View File

@@ -3,6 +3,7 @@ import { useHistory } from "react-router-dom";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { i18n } from "../../translate/i18n";
import api from "../../services/api"; import api from "../../services/api";
const useAuth = () => { const useAuth = () => {
@@ -23,7 +24,6 @@ const useAuth = () => {
history.location.pathname === "/signup" history.location.pathname === "/signup"
) { ) {
setLoading(false); setLoading(false);
return; return;
} }
try { try {
@@ -35,7 +35,7 @@ const useAuth = () => {
} catch (err) { } catch (err) {
setLoading(false); setLoading(false);
setIsAuth(false); setIsAuth(false);
toast.error("Erro de autenticação. Por favor, faça login novamente"); toast.error(i18n.t("auth.toasts.fail"));
} }
}; };
checkAuth(); checkAuth();
@@ -50,9 +50,10 @@ const useAuth = () => {
localStorage.setItem("userId", res.data.userId); localStorage.setItem("userId", res.data.userId);
api.defaults.headers.Authorization = `Bearer ${res.data.token}`; api.defaults.headers.Authorization = `Bearer ${res.data.token}`;
setIsAuth(true); setIsAuth(true);
toast.success(i18n.t("auth.toasts.success"));
history.push("/chat"); history.push("/chat");
} catch (err) { } catch (err) {
toast.error("Erro de autenticação. Verifique os dados de login"); toast.error(i18n.t("auth.toasts.fail"));
} }
}; };

View File

@@ -7,6 +7,8 @@ import { makeStyles } from "@material-ui/core/styles";
import Tickets from "../../components/Tickets/"; import Tickets from "../../components/Tickets/";
import MessagesList from "../../components/MessagesList/"; import MessagesList from "../../components/MessagesList/";
import { i18n } from "../../translate/i18n";
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
chatContainer: { chatContainer: {
flex: 1, flex: 1,
@@ -61,7 +63,7 @@ const Chat = () => {
</> </>
) : ( ) : (
<Paper square variant="outlined" className={classes.welcomeMsg}> <Paper square variant="outlined" className={classes.welcomeMsg}>
<span>Selecione um contato para começar a conversar</span> <span>{i18n.t("chat.noTicketMessage")}</span>
</Paper> </Paper>
)} )}
</Grid> </Grid>

View File

@@ -28,7 +28,7 @@ import ContactsSekeleton from "../../components/ContactsSekeleton";
import ContactModal from "../../components/ContactModal"; import ContactModal from "../../components/ContactModal";
import ConfirmationModal from "../../components/ConfirmationModal/"; import ConfirmationModal from "../../components/ConfirmationModal/";
let socket; import { i18n } from "../../translate/i18n";
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
mainContainer: { mainContainer: {
@@ -105,13 +105,7 @@ const Contacts = () => {
}, [searchParam, page, rowsPerPage]); }, [searchParam, page, rowsPerPage]);
useEffect(() => { useEffect(() => {
socket = openSocket(process.env.REACT_APP_BACKEND_URL); const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
return () => {
socket.disconnect();
};
}, []);
useEffect(() => {
socket.on("contact", data => { socket.on("contact", data => {
if ((data.action === "update" || data.action === "create") && !loading) { if ((data.action === "update" || data.action === "create") && !loading) {
updateContacts(data.contact); updateContacts(data.contact);
@@ -121,6 +115,10 @@ const Contacts = () => {
deleteContact(data.contactId); deleteContact(data.contactId);
} }
}); });
return () => {
socket.disconnect();
};
}, [loading]); }, [loading]);
const updateContacts = contact => { const updateContacts = contact => {
@@ -206,8 +204,10 @@ const Contacts = () => {
<ConfirmationModal <ConfirmationModal
title={ title={
deletingContact deletingContact
? `Deletar ${deletingContact.name}?` ? `${i18n.t("contacts.confirmationModal.deleteTitle")} ${
: `Importar contatos` deletingContact.name
}?`
: `${i18n.t("contacts.confirmationModal.importTitlte")}`
} }
open={confirmOpen} open={confirmOpen}
setOpen={setConfirmOpen} setOpen={setConfirmOpen}
@@ -218,17 +218,17 @@ const Contacts = () => {
} }
> >
{deletingContact {deletingContact
? "Tem certeza que deseja deletar este contato? Todos os tickets relacionados serão perdidos." ? `${i18n.t("contacts.confirmationModal.deleteMessage")}`
: "Deseja importas todos os contatos do telefone? Essa função é experimental, você terá que recarregar a página após a importação "} : `${i18n.t("contacts.confirmationModal.importMessage")}`}
</ConfirmationModal> </ConfirmationModal>
<div className={classes.contactsHeader}> <div className={classes.contactsHeader}>
<Typography variant="h5" gutterBottom> <Typography variant="h5" gutterBottom>
Contatos {i18n.t("contacts.title")}
</Typography> </Typography>
<div className={classes.actionButtons}> <div className={classes.actionButtons}>
<TextField <TextField
placeholder="Pesquisar..." placeholder={i18n.t("contacts.searchPlaceholder")}
type="search" type="search"
value={searchParam} value={searchParam}
onChange={handleSearch} onChange={handleSearch}
@@ -245,14 +245,14 @@ const Contacts = () => {
color="primary" color="primary"
onClick={e => setConfirmOpen(true)} onClick={e => setConfirmOpen(true)}
> >
Importar contatos {i18n.t("contacts.buttons.import")}
</Button> </Button>
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
onClick={handleOpenContactModal} onClick={handleOpenContactModal}
> >
Adicionar contato {i18n.t("contacts.buttons.add")}
</Button> </Button>
</div> </div>
</div> </div>
@@ -261,10 +261,12 @@ const Contacts = () => {
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell padding="checkbox" /> <TableCell padding="checkbox" />
<TableCell>Nome</TableCell> <TableCell>{i18n.t("contacts.table.name")}</TableCell>
<TableCell>Whatsapp</TableCell> <TableCell>{i18n.t("contacts.table.whatsapp")}</TableCell>
<TableCell>Email</TableCell> <TableCell>{i18n.t("contacts.table.email")}</TableCell>
<TableCell align="right">Ações</TableCell> <TableCell align="right">
{i18n.t("contacts.table.actions")}
</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>

View File

@@ -11,6 +11,8 @@ import {
} from "recharts"; } from "recharts";
import { startOfHour, parseISO, format } from "date-fns"; import { startOfHour, parseISO, format } from "date-fns";
import { i18n } from "../../translate/i18n";
import Title from "./Title"; import Title from "./Title";
import api from "../../services/api"; import api from "../../services/api";
@@ -69,7 +71,9 @@ const Chart = () => {
return ( return (
<React.Fragment> <React.Fragment>
<Title>{`Tickets hoje: ${tickets.length}`}</Title> <Title>{`${i18n.t("dashboard.charts.perDay.title")}${
tickets.length
}`}</Title>
<ResponsiveContainer> <ResponsiveContainer>
<BarChart <BarChart
data={chartData} data={chartData}

View File

@@ -5,8 +5,6 @@ import Avatar from "@material-ui/core/Avatar";
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import CssBaseline from "@material-ui/core/CssBaseline"; import CssBaseline from "@material-ui/core/CssBaseline";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
// import FormControlLabel from "@material-ui/core/FormControlLabel";
// import Checkbox from "@material-ui/core/Checkbox";
import Link from "@material-ui/core/Link"; import Link from "@material-ui/core/Link";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import Box from "@material-ui/core/Box"; import Box from "@material-ui/core/Box";
@@ -15,13 +13,15 @@ import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import Container from "@material-ui/core/Container"; import Container from "@material-ui/core/Container";
import { i18n } from "../../translate/i18n";
import { AuthContext } from "../../context/Auth/AuthContext"; import { AuthContext } from "../../context/Auth/AuthContext";
const Copyright = () => { const Copyright = () => {
return ( return (
<Typography variant="body2" color="textSecondary" align="center"> <Typography variant="body2" color="textSecondary" align="center">
{"Copyright © "} {"Copyright © "}
<Link color="inherit" href="https://material-ui.com/"> <Link color="inherit" href="https://economicros.com.br/">
Canove Canove
</Link>{" "} </Link>{" "}
{new Date().getFullYear()} {new Date().getFullYear()}
@@ -50,7 +50,7 @@ const useStyles = makeStyles(theme => ({
}, },
})); }));
const Login = ({ showToast }) => { const Login = () => {
const classes = useStyles(); const classes = useStyles();
const [user, setUser] = useState({ email: "", password: "" }); const [user, setUser] = useState({ email: "", password: "" });
@@ -69,7 +69,7 @@ const Login = ({ showToast }) => {
<LockOutlinedIcon /> <LockOutlinedIcon />
</Avatar> </Avatar>
<Typography component="h1" variant="h5"> <Typography component="h1" variant="h5">
Login {i18n.t("login.title")}
</Typography> </Typography>
<form <form
className={classes.form} className={classes.form}
@@ -82,7 +82,7 @@ const Login = ({ showToast }) => {
required required
fullWidth fullWidth
id="email" id="email"
label="Email" label={i18n.t("login.form.email")}
name="email" name="email"
value={user.email} value={user.email}
onChange={handleChangeInput} onChange={handleChangeInput}
@@ -95,17 +95,13 @@ const Login = ({ showToast }) => {
required required
fullWidth fullWidth
name="password" name="password"
label="Senha" label={i18n.t("login.form.password")}
type="password" type="password"
id="password" id="password"
value={user.password} value={user.password}
onChange={handleChangeInput} onChange={handleChangeInput}
autoComplete="current-password" autoComplete="current-password"
/> />
{/* <FormControlLabel
control={<Checkbox value="remember" color="primary" />}
label="Lembrar"
/> */}
<Button <Button
type="submit" type="submit"
fullWidth fullWidth
@@ -113,14 +109,9 @@ const Login = ({ showToast }) => {
color="primary" color="primary"
className={classes.submit} className={classes.submit}
> >
Entrar {i18n.t("login.buttons.submit")}
</Button> </Button>
<Grid container> <Grid container>
{/* <Grid item xs>
<Link href="#" variant="body2">
Forgot password?
</Link>
</Grid> */}
<Grid item> <Grid item>
<Link <Link
href="#" href="#"
@@ -128,7 +119,7 @@ const Login = ({ showToast }) => {
component={RouterLink} component={RouterLink}
to="/signup" to="/signup"
> >
{"Não tem uma conta? Cadastre-se!"} {i18n.t("login.buttons.register")}
</Link> </Link>
</Grid> </Grid>
</Grid> </Grid>

View File

@@ -16,6 +16,8 @@ import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import Container from "@material-ui/core/Container"; import Container from "@material-ui/core/Container";
import { i18n } from "../../translate/i18n";
import api from "../../services/api"; import api from "../../services/api";
const Copyright = () => { const Copyright = () => {
@@ -65,10 +67,10 @@ const SignUp = () => {
e.preventDefault(); e.preventDefault();
try { try {
await api.post("/auth/signup", user); await api.post("/auth/signup", user);
toast.success("Usuário criado com sucesso! Faça seu login."); toast.success(i18n.t("signup.toast.success"));
history.push("/login"); history.push("/login");
} catch (err) { } catch (err) {
toast.error("Erro ao criar usuário. Verifique os dados informados."); toast.error(i18n.t("signup.toast.fail"));
} }
}; };
@@ -80,7 +82,7 @@ const SignUp = () => {
<LockOutlinedIcon /> <LockOutlinedIcon />
</Avatar> </Avatar>
<Typography component="h1" variant="h5"> <Typography component="h1" variant="h5">
Cadastre-se {i18n.t("signup.title")}
</Typography> </Typography>
<form className={classes.form} noValidate onSubmit={handleSignUp}> <form className={classes.form} noValidate onSubmit={handleSignUp}>
<Grid container spacing={2}> <Grid container spacing={2}>
@@ -92,7 +94,7 @@ const SignUp = () => {
required required
fullWidth fullWidth
id="name" id="name"
label="Nome" label={i18n.t("signup.form.name")}
value={user.name} value={user.name}
onChange={handleChangeInput} onChange={handleChangeInput}
autoFocus autoFocus
@@ -105,7 +107,7 @@ const SignUp = () => {
required required
fullWidth fullWidth
id="email" id="email"
label="Email" label={i18n.t("signup.form.email")}
name="email" name="email"
autoComplete="email" autoComplete="email"
value={user.email} value={user.email}
@@ -118,7 +120,7 @@ const SignUp = () => {
required required
fullWidth fullWidth
name="password" name="password"
label="Senha" label={i18n.t("signup.form.password")}
type="password" type="password"
id="password" id="password"
autoComplete="current-password" autoComplete="current-password"
@@ -134,12 +136,12 @@ const SignUp = () => {
color="primary" color="primary"
className={classes.submit} className={classes.submit}
> >
Cadastrar {i18n.t("signup.buttons.submit")}
</Button> </Button>
<Grid container justify="flex-end"> <Grid container justify="flex-end">
<Grid item> <Grid item>
<Link href="#" variant="body2" component={RouterLink} to="/login"> <Link href="#" variant="body2" component={RouterLink} to="/login">
tem uma conta? Entre! {i18n.t("signup.buttons.login")}
</Link> </Link>
</Grid> </Grid>
</Grid> </Grid>

View File

@@ -10,8 +10,6 @@ import Paper from "@material-ui/core/Paper";
import SessionInfo from "../../components/SessionInfo"; import SessionInfo from "../../components/SessionInfo";
import Qrcode from "../../components/Qrcode"; import Qrcode from "../../components/Qrcode";
let socket;
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
root: { root: {
display: "flex", display: "flex",
@@ -55,14 +53,8 @@ const WhatsAuth = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
socket = openSocket(process.env.REACT_APP_BACKEND_URL); const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
return () => {
socket.disconnect();
};
}, []);
useEffect(() => {
socket.on("qrcode", data => { socket.on("qrcode", data => {
if (data.action === "update") { if (data.action === "update") {
setQrCode(data.qr); setQrCode(data.qr);
@@ -76,6 +68,10 @@ const WhatsAuth = () => {
history.push("/chat"); history.push("/chat");
} }
}); });
return () => {
socket.disconnect();
};
}, [history]); }, [history]);
return ( return (

View File

@@ -0,0 +1,14 @@
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { messages } from "./languages";
i18n.use(LanguageDetector).init({
debug: false,
defaultNS: ["translations"],
fallbackLng: "pt",
ns: ["translations"],
resources: messages,
});
export { i18n };

View File

@@ -0,0 +1,24 @@
const messages = {
en: {
translations: {
signup: {
title: "Signup",
toasts: {
success: "User sucessfully registered. Login!",
fail: "Error creating user account. Verify your data.",
},
form: {
name: "Name",
email: "Email",
password: "Password",
},
buttons: {
submit: "SIGNUP",
login: "Already registred? Go to login!",
},
},
},
},
};
export { messages };

View File

@@ -0,0 +1,9 @@
import { messages as portugueseMessages } from "./pt";
import { messages as englishMessages } from "./en";
const messages = {
...portugueseMessages,
...englishMessages,
};
export { messages };

View File

@@ -0,0 +1,154 @@
const messages = {
pt: {
translations: {
signup: {
title: "Cadastre-se",
toasts: {
success: "Usuário criado com sucesso! Faça seu login!!!.",
fail: "Erro ao criar usuário. Verifique os dados informados.",
},
form: {
name: "Nome",
email: "Email",
password: "Senha",
},
buttons: {
submit: "Cadastrar",
login: "Já tem uma conta? Entre!",
},
},
login: {
title: "Login",
form: {
email: "Email",
password: "Senha",
},
buttons: {
submit: "Entrar",
register: "Não tem um conta? Cadastre-se!",
},
},
auth: {
toasts: {
success: "Login efetuado com sucesso!",
fail: "Erro de autenticação. Por favor, faça login novamente",
},
},
dashboard: {
charts: {
perDay: {
title: "Tickets hoje: ",
},
},
},
sessionInfo: {
status: "Status: ",
battery: "Bateria: ",
charging: "Carregando: ",
},
qrCode: {
message: "Leia o QrCode para iniciar a sessão",
},
contacts: {
title: "Contatos",
searchPlaceholder: "Pesquisar...",
confirmationModal: {
deleteTitle: "Deletar ",
importTitlte: "Importar contatos",
deleteMessage:
"Tem certeza que deseja deletar este contato? Todos os tickets relacionados serão perdidos.",
importMessage:
"Deseja importas todos os contatos do telefone? Essa função é experimental, você terá que recarregar a página após a importação.",
},
buttons: {
import: "Importar Contatos",
add: "Adicionar Contato",
},
table: {
name: "Nome",
whatsapp: "WhatsApp",
email: "Email",
actions: "Ações",
},
},
contactModal: {
title: {
add: "Adicionar contato",
edit: "Editar contato",
},
form: {
mainInfo: "Dados do contato",
extraInfo: "Informações adicionais",
name: "Nome",
number: "Número do Whatsapp",
email: "Email",
extraName: "Nome do campo",
extraValue: "Valor",
},
buttons: {
addExtraInfo: "Adicionar informação",
okAdd: "Adicionar",
okEdit: "Salvar",
cancel: "Cancelar",
},
},
chat: {
noTicketMessage: "Selecione um ticket para começar a conversar.",
},
tickets: {
toasts: {
deleted: "O ticket que você estava foi deletado.",
},
notification: {
message: "Mensagem de",
},
tabs: {
open: {
title: "Inbox",
assignedHeader: "Atendendo",
pendingHeader: "Aguardando",
openNoTicketsTitle: "Pronto pra mais?",
openNoTicketsMessage: "Aceite um ticket da fila para começar.",
pendingNoTicketsTitle: "Tudo resolvido!",
pendingNoTicketsMessage: "Nenhum ticket pendente.",
},
closed: { title: "Resolvidos" },
search: {
title: "Busca",
noTicketsTitle: "Nada encontrado!",
noTicketsMessage: "Tente pesquisar por outro termo.",
},
},
search: {
placeholder: "Pesquisar tickets e mensagens.",
},
buttons: {
showAll: "Todos",
},
},
ticketsList: {
buttons: {
accept: "Aceitar",
},
},
newTicketModal: {
title: "Criar Ticket",
fieldLabel: "Digite para pesquisar o contato",
buttons: {
ok: "Salvar",
cancel: "Cancelar",
},
},
mainDrawer: {
listItems: {
dashboard: "Dashboard",
connection: "Conexão",
tickets: "Tickets",
contacts: "Contatos",
},
},
},
},
};
export { messages };

File diff suppressed because it is too large Load Diff