diff --git a/src/pages/rules.tsx b/src/pages/rules.tsx index 60f6853446ca832815d3c6fb951a62cd9bd8478b..72f54fbfb544dacafd898647fc9592e4a8f5f1a7 100644 --- a/src/pages/rules.tsx +++ b/src/pages/rules.tsx @@ -1,71 +1,45 @@ import { useState } from "react"; -import { - Box, - Button, - Grid, - Slide, - Snackbar, - TextField, - Typography, -} from "@mui/material"; +import { Box, Button, TextField, Typography } from "@mui/material"; import { importProfile } from "../services/command"; +import useNotice from "../utils/use-notice"; const RulesPage = () => { const [url, setUrl] = useState(""); - const [message, setMessage] = useState(""); const [disabled, setDisabled] = useState(false); + const [notice, noticeElement] = useNotice(); + const onClick = () => { if (!url) return; setUrl(""); setDisabled(true); importProfile(url) - .then(() => setMessage("Successfully import profile.")) - .catch(() => setMessage("Failed to import profile.")) + .then(() => notice.success("Successfully import profile.")) + .catch(() => notice.error("Failed to import profile.")) .finally(() => setDisabled(false)); }; return ( <Box sx={{ width: 0.9, maxWidth: "850px", mx: "auto", mb: 2 }}> - <Typography variant="h4" component="h1" sx={{ py: 2 }}> + <Typography variant="h4" component="h1" sx={{ py: 2, mb: 1 }}> Rules </Typography> - <Grid - container - spacing={2} - justifyContent="space-between" - alignItems="center" - > - <Grid item xs={9}> - <TextField - label="Profile Url" - size="medium" - fullWidth - value={url} - onChange={(e) => setUrl(e.target.value)} - /> - </Grid> - <Grid item> - <Button - disabled={disabled} - size="large" - variant="contained" - onClick={onClick} - > - Import - </Button> - </Grid> - </Grid> + <Box sx={{ display: "flex" }}> + <TextField + label="Profile URL" + size="small" + fullWidth + value={url} + onChange={(e) => setUrl(e.target.value)} + sx={{ mr: 4 }} + /> + <Button disabled={disabled} variant="contained" onClick={onClick}> + Import + </Button> + </Box> - <Snackbar - open={!!message} - anchorOrigin={{ vertical: "top", horizontal: "right" }} - autoHideDuration={3000} - onClose={() => setMessage("")} - message={message} - TransitionComponent={(p) => <Slide {...p} direction="left" />} - /> + {noticeElement} </Box> ); }; diff --git a/src/utils/use-notice.tsx b/src/utils/use-notice.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4999f0547162d5c2daea822a8abec7e4ca714baa --- /dev/null +++ b/src/utils/use-notice.tsx @@ -0,0 +1,55 @@ +import { useMemo, useState } from "react"; +import { IconButton, Slide, Snackbar } from "@mui/material"; +import { Close } from "@mui/icons-material"; + +interface NoticeInstance { + info: (msg: string) => void; + error: (msg: string) => void; + success: (msg: string) => void; +} + +const useNotice = () => { + const [message, setMessage] = useState(""); + const [level, setLevel] = useState<"info" | "error" | "success">("info"); + + const handleClose = (_e: any, reason: string) => { + if (reason !== "clickaway") setMessage(""); + }; + + const element = useMemo( + () => ( + <Snackbar + open={!!message} + anchorOrigin={{ vertical: "top", horizontal: "right" }} + autoHideDuration={3000} + onClose={handleClose} + message={message} + TransitionComponent={(p) => <Slide {...p} direction="left" />} + action={ + <IconButton + size="small" + color="inherit" + onClick={() => setMessage("")} + > + <Close fontSize="small" /> + </IconButton> + } + /> + ), + [message] + ); + + const instance = (Object.fromEntries( + (["info", "error", "success"] as const).map((item) => [ + item, + (msg: string) => { + setLevel(item); + setMessage(msg); + }, + ]) + ) as unknown) as NoticeInstance; + + return [instance, element] as const; +}; + +export default useNotice;