improvement: better user feedback on clicking buttons

This commit is contained in:
canove
2020-10-08 14:10:34 -03:00
parent 31bf85635d
commit c170c0c8ae
6 changed files with 145 additions and 102 deletions

View File

@@ -0,0 +1,35 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import { CircularProgress, Button } from "@material-ui/core";
const useStyles = makeStyles(theme => ({
button: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
}));
const ButtonWithSpinner = ({ loading, children, ...rest }) => {
const classes = useStyles();
return (
<Button className={classes.button} disabled={loading} {...rest}>
{children}
{loading && (
<CircularProgress size={24} className={classes.buttonProgress} />
)}
</Button>
);
};
export default ButtonWithSpinner;

View File

@@ -195,55 +195,54 @@ const MessageInput = ({ ticketStatus }) => {
setLoading(false); setLoading(false);
}; };
const handleStartRecording = () => { const handleStartRecording = async () => {
navigator.getUserMedia(
{ audio: true },
() => {
Mp3Recorder.start()
.then(() => {
setRecording(true);
})
.catch(e => console.error(e));
},
() => {
console.log("Permission Denied");
}
);
};
const handleUploadAudio = () => {
setLoading(true); setLoading(true);
Mp3Recorder.stop() try {
.getMp3() await navigator.mediaDevices.getUserMedia({ audio: true });
.then(async ([buffer, blob]) => { await Mp3Recorder.start();
if (blob.size < 10000) { setRecording(true);
setLoading(false); setLoading(false);
setRecording(false); } catch (err) {
return; console.log(err);
} setLoading(false);
const formData = new FormData(); }
const filename = `${new Date().getTime()}.mp3`;
formData.append("media", blob, filename);
formData.append("body", filename);
formData.append("fromMe", true);
try {
await api.post(`/messages/${ticketId}`, formData);
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
setRecording(false);
setLoading(false);
})
.catch(err => console.log(err));
}; };
const handleCancelAudio = () => { const handleUploadAudio = async () => {
Mp3Recorder.stop() setLoading(true);
.getMp3() try {
.then(() => setRecording(false)); const [, blob] = await Mp3Recorder.stop().getMp3();
if (blob.size < 10000) {
setLoading(false);
setRecording(false);
return;
}
const formData = new FormData();
const filename = `${new Date().getTime()}.mp3`;
formData.append("media", blob, filename);
formData.append("body", filename);
formData.append("fromMe", true);
await api.post(`/messages/${ticketId}`, formData);
setRecording(false);
setLoading(false);
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
};
const handleCancelAudio = async () => {
try {
await Mp3Recorder.stop().getMp3();
setRecording(false);
} catch (err) {
console.log(err);
}
}; };
if (media.preview) if (media.preview)

View File

@@ -13,12 +13,12 @@ import Autocomplete, {
createFilterOptions, createFilterOptions,
} from "@material-ui/lab/Autocomplete"; } from "@material-ui/lab/Autocomplete";
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 { makeStyles } from "@material-ui/core/styles";
import { i18n } from "../../translate/i18n"; import { i18n } from "../../translate/i18n";
import api from "../../services/api"; import api from "../../services/api";
import ButtonWithSpinner from "../ButtonWithSpinner";
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
root: { root: {
@@ -26,19 +26,11 @@ const useStyles = makeStyles(theme => ({
flexWrap: "wrap", flexWrap: "wrap",
}, },
btnWrapper: { // btnWrapper: {
// margin: theme.spacing(1), // // margin: theme.spacing(1),
position: "relative", // // position: "relative",
}, // display: "flex",
// },
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
})); }));
const filterOptions = createFilterOptions({ const filterOptions = createFilterOptions({
@@ -163,21 +155,14 @@ const NewTicketModal = ({ modalOpen, onClose }) => {
> >
{i18n.t("newTicketModal.buttons.cancel")} {i18n.t("newTicketModal.buttons.cancel")}
</Button> </Button>
<Button <ButtonWithSpinner
variant="contained"
type="submit" type="submit"
color="primary" color="primary"
disabled={loading} loading={loading}
variant="contained"
className={classes.btnWrapper}
> >
{i18n.t("newTicketModal.buttons.ok")} {i18n.t("newTicketModal.buttons.ok")}
{loading && ( </ButtonWithSpinner>
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</DialogActions> </DialogActions>
</form> </form>
</Dialog> </Dialog>

View File

@@ -3,12 +3,13 @@ import { useHistory } from "react-router-dom";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import { Button, IconButton } from "@material-ui/core"; import { IconButton } from "@material-ui/core";
import { MoreVert, Replay } from "@material-ui/icons"; import { MoreVert, Replay } from "@material-ui/icons";
import { i18n } from "../../translate/i18n"; import { i18n } from "../../translate/i18n";
import api from "../../services/api"; import api from "../../services/api";
import TicketOptionsMenu from "../TicketOptionsMenu"; import TicketOptionsMenu from "../TicketOptionsMenu";
import ButtonWithSpinner from "../ButtonWithSpinner";
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
actionButtons: { actionButtons: {
@@ -27,6 +28,7 @@ const TicketActionButtons = ({ ticket }) => {
const history = useHistory(); const history = useHistory();
const userId = +localStorage.getItem("userId"); const userId = +localStorage.getItem("userId");
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const [loading, setLoading] = useState(false);
const ticketOptionsMenuOpen = Boolean(anchorEl); const ticketOptionsMenuOpen = Boolean(anchorEl);
const handleOpenTicketOptionsMenu = e => { const handleOpenTicketOptionsMenu = e => {
@@ -38,18 +40,21 @@ const TicketActionButtons = ({ ticket }) => {
}; };
const handleUpdateTicketStatus = async (e, status, userId) => { const handleUpdateTicketStatus = async (e, status, userId) => {
setLoading(true);
try { try {
await api.put(`/tickets/${ticket.id}`, { await api.put(`/tickets/${ticket.id}`, {
status: status, status: status,
userId: userId || null, userId: userId || null,
}); });
setLoading(false);
if (status === "open") { if (status === "open") {
history.push(`/tickets/${ticket.id}`); history.push(`/tickets/${ticket.id}`);
} else { } else {
history.push("/tickets"); history.push("/tickets");
} }
} catch (err) { } catch (err) {
setLoading(false);
console.log(err); console.log(err);
if (err.response && err.response.data && err.response.data.error) { if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error); toast.error(err.response.data.error);
@@ -60,31 +65,34 @@ const TicketActionButtons = ({ ticket }) => {
return ( return (
<div className={classes.actionButtons}> <div className={classes.actionButtons}>
{ticket.status === "closed" && ( {ticket.status === "closed" && (
<Button <ButtonWithSpinner
loading={loading}
startIcon={<Replay />} startIcon={<Replay />}
size="small" size="small"
onClick={e => handleUpdateTicketStatus(e, "open", userId)} onClick={e => handleUpdateTicketStatus(e, "open", userId)}
> >
{i18n.t("messagesList.header.buttons.reopen")} {i18n.t("messagesList.header.buttons.reopen")}
</Button> </ButtonWithSpinner>
)} )}
{ticket.status === "open" && ( {ticket.status === "open" && (
<> <>
<Button <ButtonWithSpinner
loading={loading}
startIcon={<Replay />} startIcon={<Replay />}
size="small" size="small"
onClick={e => handleUpdateTicketStatus(e, "pending", null)} onClick={e => handleUpdateTicketStatus(e, "pending", null)}
> >
{i18n.t("messagesList.header.buttons.return")} {i18n.t("messagesList.header.buttons.return")}
</Button> </ButtonWithSpinner>
<Button <ButtonWithSpinner
loading={loading}
size="small" size="small"
variant="contained" variant="contained"
color="primary" color="primary"
onClick={e => handleUpdateTicketStatus(e, "closed", userId)} onClick={e => handleUpdateTicketStatus(e, "closed", userId)}
> >
{i18n.t("messagesList.header.buttons.resolve")} {i18n.t("messagesList.header.buttons.resolve")}
</Button> </ButtonWithSpinner>
<IconButton onClick={handleOpenTicketOptionsMenu}> <IconButton onClick={handleOpenTicketOptionsMenu}>
<MoreVert /> <MoreVert />
</IconButton> </IconButton>
@@ -97,14 +105,15 @@ const TicketActionButtons = ({ ticket }) => {
</> </>
)} )}
{ticket.status === "pending" && ( {ticket.status === "pending" && (
<Button <ButtonWithSpinner
loading={loading}
size="small" size="small"
variant="contained" variant="contained"
color="primary" color="primary"
onClick={e => handleUpdateTicketStatus(e, "open", userId)} onClick={e => handleUpdateTicketStatus(e, "open", userId)}
> >
{i18n.t("messagesList.header.buttons.accept")} {i18n.t("messagesList.header.buttons.accept")}
</Button> </ButtonWithSpinner>
)} )}
</div> </div>
); );

View File

@@ -16,12 +16,16 @@ const useStyles = makeStyles(theme => ({
const TicketHeader = ({ loading, children }) => { const TicketHeader = ({ loading, children }) => {
const classes = useStyles(); const classes = useStyles();
if (loading) return <TicketHeaderSkeleton />;
return ( return (
<Card square className={classes.ticketHeader}> <>
{children} {loading ? (
</Card> <TicketHeaderSkeleton />
) : (
<Card square className={classes.ticketHeader}>
{children}
</Card>
)}
</>
); );
}; };

View File

@@ -1,4 +1,4 @@
import React from "react"; import React, { useState, useEffect, useRef } from "react";
import { useHistory, useParams } from "react-router-dom"; import { useHistory, useParams } from "react-router-dom";
import { parseISO, format, isSameDay } from "date-fns"; import { parseISO, format, isSameDay } from "date-fns";
@@ -12,23 +12,15 @@ import Typography from "@material-ui/core/Typography";
import Avatar from "@material-ui/core/Avatar"; import Avatar from "@material-ui/core/Avatar";
import Divider from "@material-ui/core/Divider"; 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 { i18n } from "../../translate/i18n"; import { i18n } from "../../translate/i18n";
import api from "../../services/api"; import api from "../../services/api";
import ButtonWithSpinner from "../ButtonWithSpinner";
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
ticket: { ticket: {
position: "relative", position: "relative",
"& .hidden-button": {
display: "none",
},
"&:hover .hidden-button": {
display: "flex",
position: "absolute",
left: "50%",
},
}, },
noTicketsDiv: { noTicketsDiv: {
display: "flex", display: "flex",
@@ -83,23 +75,41 @@ const useStyles = makeStyles(theme => ({
color: "white", color: "white",
backgroundColor: green[500], backgroundColor: green[500],
}, },
acceptButton: {
position: "absolute",
left: "50%",
},
})); }));
const TicketListItem = ({ ticket }) => { const TicketListItem = ({ ticket }) => {
const classes = useStyles(); const classes = useStyles();
const history = useHistory(); const history = useHistory();
const userId = +localStorage.getItem("userId"); const userId = +localStorage.getItem("userId");
const [loading, setLoading] = useState(false);
const { ticketId } = useParams(); const { ticketId } = useParams();
const isMounted = useRef(true);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
const handleAcepptTicket = async ticketId => { const handleAcepptTicket = async ticketId => {
setLoading(true);
try { try {
await api.put(`/tickets/${ticketId}`, { await api.put(`/tickets/${ticketId}`, {
status: "open", status: "open",
userId: userId, userId: userId,
}); });
} catch (err) { } catch (err) {
setLoading(false);
alert(err); alert(err);
} }
if (isMounted.current) {
setLoading(false);
}
history.push(`/tickets/${ticketId}`); history.push(`/tickets/${ticketId}`);
}; };
@@ -180,17 +190,18 @@ const TicketListItem = ({ ticket }) => {
</span> </span>
} }
/> />
{ticket.status === "pending" ? ( {ticket.status === "pending" && (
<Button <ButtonWithSpinner
variant="contained"
size="small"
color="primary" color="primary"
className="hidden-button" variant="contained"
className={classes.acceptButton}
size="small"
loading={loading}
onClick={e => handleAcepptTicket(ticket.id)} onClick={e => handleAcepptTicket(ticket.id)}
> >
{i18n.t("ticketsList.buttons.accept")} {i18n.t("ticketsList.buttons.accept")}
</Button> </ButtonWithSpinner>
) : null} )}
</ListItem> </ListItem>
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
</React.Fragment> </React.Fragment>