refactor: notice caller

This commit is contained in:
GyDi 2022-01-16 17:56:43 +08:00
parent 9ca83d3291
commit 4e2cb30db7
No known key found for this signature in database
GPG Key ID: 1C95E0D3467B3084
3 changed files with 92 additions and 75 deletions

88
src/components/notice.tsx Normal file
View File

@ -0,0 +1,88 @@
import ReactDOM from "react-dom";
import { ReactNode, useState } from "react";
import { Box, IconButton, Slide, Snackbar, Typography } from "@mui/material";
import { Close, CheckCircleRounded, ErrorRounded } from "@mui/icons-material";
interface InnerProps {
type: string;
duration?: number;
message: ReactNode;
onClose: () => void;
}
const NoticeInner = (props: InnerProps) => {
const { type, message, duration = 2000, onClose } = props;
const [visible, setVisible] = useState(true);
const onBtnClose = () => {
setVisible(false);
onClose();
};
const onAutoClose = (_e: any, reason: string) => {
if (reason !== "clickaway") onBtnClose();
};
const msgElement =
type === "info" ? (
message
) : (
<Box sx={{ display: "flex", alignItems: "center" }}>
{type === "error" && <ErrorRounded color="error" />}
{type === "success" && <CheckCircleRounded color="success" />}
<Typography sx={{ ml: 1 }}>{message}</Typography>
</Box>
);
return (
<Snackbar
open={visible}
anchorOrigin={{ vertical: "top", horizontal: "right" }}
autoHideDuration={duration}
onClose={onAutoClose}
message={msgElement}
sx={{ maxWidth: 360 }}
TransitionComponent={(p) => <Slide {...p} direction="left" />}
transitionDuration={200}
action={
<IconButton size="small" color="inherit" onClick={onBtnClose}>
<Close fontSize="inherit" />
</IconButton>
}
/>
);
};
interface NoticeInstance {
(props: Omit<InnerProps, "onClose">): void;
info(message: ReactNode, duration?: number): void;
error(message: ReactNode, duration?: number): void;
success(message: ReactNode, duration?: number): void;
}
let parent: HTMLDivElement = null!;
// @ts-ignore
const Notice: NoticeInstance = (props) => {
if (!parent) {
parent = document.createElement("div");
document.body.appendChild(parent);
}
const container = document.createElement("div");
parent.appendChild(container);
const onUnmount = () => {
const result = ReactDOM.unmountComponentAtNode(container);
if (result && parent) parent.removeChild(container);
};
ReactDOM.render(<NoticeInner {...props} onClose={onUnmount} />, container);
};
(["info", "error", "success"] as const).forEach((type) => {
Notice[type] = (message, duration) => Notice({ type, message, duration });
});
export default Notice;

View File

@ -9,14 +9,13 @@ import {
} from "../services/cmds"; } from "../services/cmds";
import { getProxies, updateProxy } from "../services/api"; import { getProxies, updateProxy } from "../services/api";
import noop from "../utils/noop"; import noop from "../utils/noop";
import useNotice from "../utils/use-notice"; import Notice from "../components/notice";
import BasePage from "../components/base-page"; import BasePage from "../components/base-page";
import ProfileItemComp from "../components/profile-item"; import ProfileItemComp from "../components/profile-item";
const ProfilePage = () => { const ProfilePage = () => {
const [url, setUrl] = useState(""); const [url, setUrl] = useState("");
const [disabled, setDisabled] = useState(false); const [disabled, setDisabled] = useState(false);
const [notice, noticeElement] = useNotice();
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const { data: profiles = {} } = useSWR("getProfiles", getProfiles); const { data: profiles = {} } = useSWR("getProfiles", getProfiles);
@ -72,9 +71,9 @@ const ProfilePage = () => {
await importProfile(url); await importProfile(url);
mutate("getProfiles", getProfiles()); mutate("getProfiles", getProfiles());
if (!profiles.items?.length) selectProfile(0).catch(noop); if (!profiles.items?.length) selectProfile(0).catch(noop);
notice.success("Successfully import profile."); Notice.success("Successfully import profile.");
} catch { } catch {
notice.error("Failed to import profile."); Notice.error("Failed to import profile.");
} finally { } finally {
setDisabled(false); setDisabled(false);
} }
@ -108,7 +107,7 @@ const ProfilePage = () => {
fullWidth fullWidth
value={url} value={url}
onChange={(e) => setUrl(e.target.value)} onChange={(e) => setUrl(e.target.value)}
sx={{ mr: 4 }} sx={{ mr: 2 }}
/> />
<Button <Button
disabled={!url || disabled} disabled={!url || disabled}
@ -131,8 +130,6 @@ const ProfilePage = () => {
</Grid> </Grid>
))} ))}
</Grid> </Grid>
{noticeElement}
</BasePage> </BasePage>
); );
}; };

View File

@ -1,68 +0,0 @@
import { useMemo, useState } from "react";
import { Box, IconButton, Slide, Snackbar } from "@mui/material";
import { Close, CheckCircleRounded, ErrorRounded } 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 msgElement =
level === "info" ? (
message
) : (
<Box sx={{ display: "flex", alignItems: "center" }}>
{level === "error" && <ErrorRounded color="error" />}
{level === "success" && <CheckCircleRounded color="success" />}
<span style={{ marginLeft: 4 }}>{message}</span>
</Box>
);
const element = useMemo(
() => (
<Snackbar
open={!!message}
anchorOrigin={{ vertical: "top", horizontal: "right" }}
autoHideDuration={3000}
onClose={handleClose}
message={msgElement}
sx={{ maxWidth: 360 }}
TransitionComponent={(p) => <Slide {...p} direction="left" />}
transitionDuration={200}
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;