diff --git a/web/package.json b/web/package.json index 33da8e3..b3c4558 100644 --- a/web/package.json +++ b/web/package.json @@ -19,6 +19,7 @@ "qs": "^6.11.0", "react": "^18.1.0", "react-dom": "^18.1.0", + "react-hot-toast": "^2.4.0", "react-redux": "^8.0.1", "react-router-dom": "^6.4.0" }, diff --git a/web/src/App.tsx b/web/src/App.tsx index 3bba82a..8190ca2 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,4 +1,5 @@ import { CssVarsProvider } from "@mui/joy/styles"; +import { Toaster } from "react-hot-toast"; import { RouterProvider } from "react-router-dom"; import router from "./router"; @@ -6,6 +7,7 @@ function App() { return ( + ); } diff --git a/web/src/components/ChangePasswordDialog.tsx b/web/src/components/ChangePasswordDialog.tsx index 7e6aa43..6fac33e 100644 --- a/web/src/components/ChangePasswordDialog.tsx +++ b/web/src/components/ChangePasswordDialog.tsx @@ -1,10 +1,10 @@ import { Button, Input, Modal, ModalDialog } from "@mui/joy"; import { useState } from "react"; +import { toast } from "react-hot-toast"; import { validate, ValidatorConfig } from "../helpers/validator"; import useLoading from "../hooks/useLoading"; import { userService } from "../services"; import Icon from "./Icon"; -import toastHelper from "./Toast"; const validateConfig: ValidatorConfig = { minLength: 3, @@ -39,19 +39,19 @@ const ChangePasswordDialog: React.FC = (props: Props) => { const handleSaveBtnClick = async () => { if (newPassword === "" || newPasswordAgain === "") { - toastHelper.error("Please fill all inputs"); + toast.error("Please fill all inputs"); return; } if (newPassword !== newPasswordAgain) { - toastHelper.error("Not matched"); + toast.error("Not matched"); setNewPasswordAgain(""); return; } const passwordValidResult = validate(newPassword, validateConfig); if (!passwordValidResult.result) { - toastHelper.error("New password is invalid"); + toast.error("New password is invalid"); return; } @@ -63,10 +63,10 @@ const ChangePasswordDialog: React.FC = (props: Props) => { password: newPassword, }); onClose(); - toastHelper.info("Password changed"); + toast("Password changed"); } catch (error: any) { console.error(error); - toastHelper.error(JSON.stringify(error.response.data)); + toast.error(JSON.stringify(error.response.data)); } requestState.setFinish(); }; diff --git a/web/src/components/CreateShortcutDialog.tsx b/web/src/components/CreateShortcutDialog.tsx index 55797cc..38699ee 100644 --- a/web/src/components/CreateShortcutDialog.tsx +++ b/web/src/components/CreateShortcutDialog.tsx @@ -1,9 +1,9 @@ import { Button, Input, Modal, ModalDialog, Radio, RadioGroup } from "@mui/joy"; import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { shortcutService } from "../services"; import useLoading from "../hooks/useLoading"; import Icon from "./Icon"; -import toastHelper from "./Toast"; interface Props { workspaceId: WorkspaceId; @@ -76,7 +76,7 @@ const CreateShortcutDialog: React.FC = (props: Props) => { const handleSaveBtnClick = async () => { if (!state.shortcutCreate.name) { - toastHelper.error("Name is required"); + toast.error("Name is required"); return; } @@ -100,7 +100,7 @@ const CreateShortcutDialog: React.FC = (props: Props) => { } } catch (error: any) { console.error(error); - toastHelper.error(JSON.stringify(error.response.data)); + toast.error(JSON.stringify(error.response.data)); } }; diff --git a/web/src/components/CreateWorkspaceDialog.tsx b/web/src/components/CreateWorkspaceDialog.tsx index ac72a96..0d20d15 100644 --- a/web/src/components/CreateWorkspaceDialog.tsx +++ b/web/src/components/CreateWorkspaceDialog.tsx @@ -1,9 +1,9 @@ import { Button, Input, Modal, ModalDialog } from "@mui/joy"; import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { workspaceService } from "../services"; import useLoading from "../hooks/useLoading"; import Icon from "./Icon"; -import toastHelper from "./Toast"; interface Props { workspaceId?: WorkspaceId; @@ -55,11 +55,11 @@ const CreateWorkspaceDialog: React.FC = (props: Props) => { const handleSaveBtnClick = async () => { if (!state.workspaceCreate.name) { - toastHelper.error("ID is required"); + toast.error("ID is required"); return; } if (!state.workspaceCreate.title) { - toastHelper.error("Title is required"); + toast.error("Title is required"); return; } @@ -84,7 +84,7 @@ const CreateWorkspaceDialog: React.FC = (props: Props) => { } } catch (error: any) { console.error(error); - toastHelper.error(JSON.stringify(error.response.data)); + toast.error(JSON.stringify(error.response.data)); } requestState.setFinish(); }; diff --git a/web/src/components/MemberListView.tsx b/web/src/components/MemberListView.tsx index 0d23363..248600f 100644 --- a/web/src/components/MemberListView.tsx +++ b/web/src/components/MemberListView.tsx @@ -1,10 +1,10 @@ import { useEffect } from "react"; +import { toast } from "react-hot-toast"; import { deleteWorkspaceUser, upsertWorkspaceUser } from "../helpers/api"; import { useAppSelector } from "../store"; import { unknownWorkspace, unknownWorkspaceUser } from "../store/modules/workspace"; import useLoading from "../hooks/useLoading"; import { workspaceService } from "../services"; -import toastHelper from "./Toast"; import Dropdown from "./common/Dropdown"; import { showCommonDialog } from "./Alert"; import Icon from "./Icon"; @@ -26,7 +26,7 @@ const MemberListView: React.FC = (props: Props) => { useEffect(() => { const workspace = workspaceService.getWorkspaceById(workspaceId); if (!workspace) { - toastHelper.error("workspace not found"); + toast.error("workspace not found"); return; } @@ -35,7 +35,7 @@ const MemberListView: React.FC = (props: Props) => { const handleWorkspaceUserRoleChange = async (workspaceUser: WorkspaceUser, role: Role) => { if (workspaceUser.userId === currentUser.userId) { - toastHelper.error("Cannot change yourself."); + toast.error("Cannot change yourself."); return; } diff --git a/web/src/components/ShortcutListView.tsx b/web/src/components/ShortcutListView.tsx index f8f6beb..9678907 100644 --- a/web/src/components/ShortcutListView.tsx +++ b/web/src/components/ShortcutListView.tsx @@ -1,6 +1,7 @@ import { Tooltip } from "@mui/joy"; import copy from "copy-to-clipboard"; import { useState } from "react"; +import { toast } from "react-hot-toast"; import { UNKNOWN_ID } from "../helpers/consts"; import { shortcutService, workspaceService } from "../services"; import { useAppSelector } from "../store"; @@ -8,7 +9,6 @@ import { unknownWorkspace, unknownWorkspaceUser } from "../store/modules/workspa import { absolutifyLink } from "../helpers/utils"; import { showCommonDialog } from "./Alert"; import Icon from "./Icon"; -import toastHelper from "./Toast"; import Dropdown from "./common/Dropdown"; import CreateShortcutDialog from "./CreateShortcutDialog"; @@ -38,7 +38,7 @@ const ShortcutListView: React.FC = (props: Props) => { const handleCopyButtonClick = (shortcut: Shortcut) => { const workspace = workspaceService.getWorkspaceById(workspaceId); copy(absolutifyLink(`/${workspace?.name}/${shortcut.name}`)); - toastHelper.error("Shortcut link copied to clipboard."); + toast.success("Shortcut link copied to clipboard."); }; const handleEditShortcutButtonClick = (shortcut: Shortcut) => { diff --git a/web/src/components/Toast.tsx b/web/src/components/Toast.tsx deleted file mode 100644 index 729f668..0000000 --- a/web/src/components/Toast.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useEffect } from "react"; -import { createRoot, Root } from "react-dom/client"; -import "../css/toast.css"; - -type ToastType = "normal" | "success" | "info" | "error"; - -type ToastConfig = { - type: ToastType; - content: string; - duration: number; -}; - -type ToastItemProps = { - type: ToastType; - content: string; - duration: number; - destory: FunctionType; -}; - -const Toast: React.FC = (props: ToastItemProps) => { - const { destory, duration } = props; - - useEffect(() => { - if (duration > 0) { - setTimeout(() => { - destory(); - }, duration); - } - }, []); - - return ( -
-

{props.content}

-
- ); -}; - -// toast animation duration. -const TOAST_ANIMATION_DURATION = 400; - -const initialToastHelper = () => { - const shownToastContainers: [Root, HTMLDivElement][] = []; - let shownToastAmount = 0; - - const wrapperClassName = "toast-list-container"; - const tempDiv = document.createElement("div"); - tempDiv.className = wrapperClassName; - document.body.appendChild(tempDiv); - const toastWrapper = tempDiv; - - const showToast = (config: ToastConfig) => { - const tempDiv = document.createElement("div"); - const toast = createRoot(tempDiv); - tempDiv.className = `toast-wrapper ${config.type}`; - toastWrapper.appendChild(tempDiv); - shownToastAmount++; - shownToastContainers.push([toast, tempDiv]); - - const cbs = { - destory: () => { - tempDiv.classList.add("destory"); - - setTimeout(() => { - if (!tempDiv.parentElement) { - return; - } - - shownToastAmount--; - if (shownToastAmount === 0) { - for (const [root, tempDiv] of shownToastContainers) { - root.unmount(); - tempDiv.remove(); - } - shownToastContainers.splice(0, shownToastContainers.length); - } - }, TOAST_ANIMATION_DURATION); - }, - }; - - toast.render(); - - setTimeout(() => { - tempDiv.classList.add("showup"); - }, 10); - - return cbs; - }; - - const info = (content: string, duration = 3000) => { - return showToast({ type: "normal", content, duration }); - }; - - const success = (content: string, duration = 3000) => { - return showToast({ type: "success", content, duration }); - }; - - const error = (content: string, duration = 3000) => { - return showToast({ type: "error", content, duration }); - }; - - return { - info, - success, - error, - }; -}; - -const toastHelper = initialToastHelper(); - -export default toastHelper; diff --git a/web/src/components/UpsertWorkspaceUserDialog.tsx b/web/src/components/UpsertWorkspaceUserDialog.tsx index 8258fcc..33d2ba5 100644 --- a/web/src/components/UpsertWorkspaceUserDialog.tsx +++ b/web/src/components/UpsertWorkspaceUserDialog.tsx @@ -1,11 +1,11 @@ import { Button, Input, Modal, ModalDialog, Radio, RadioGroup } from "@mui/joy"; import { useState } from "react"; +import { toast } from "react-hot-toast"; import { workspaceService } from "../services"; import { UNKNOWN_ID } from "../helpers/consts"; import { upsertWorkspaceUser } from "../helpers/api"; import useLoading from "../hooks/useLoading"; import Icon from "./Icon"; -import toastHelper from "./Toast"; interface Props { workspaceId: WorkspaceId; @@ -50,7 +50,7 @@ const UpsertWorkspaceUserDialog: React.FC = (props: Props) => { const handleSaveBtnClick = async () => { if (!state.workspaceUserUpsert.userId) { - toastHelper.error("User ID is required"); + toast.error("User ID is required"); return; } @@ -69,7 +69,7 @@ const UpsertWorkspaceUserDialog: React.FC = (props: Props) => { } } catch (error: any) { console.error(error); - toastHelper.error(JSON.stringify(error.response.data)); + toast.error(JSON.stringify(error.response.data)); } requestState.setFinish(); }; diff --git a/web/src/components/WorkspaceSetting.tsx b/web/src/components/WorkspaceSetting.tsx index 6a166a2..46f26aa 100644 --- a/web/src/components/WorkspaceSetting.tsx +++ b/web/src/components/WorkspaceSetting.tsx @@ -1,13 +1,13 @@ import { Button } from "@mui/joy"; import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; +import { toast } from "react-hot-toast"; import { deleteWorkspaceUser } from "../helpers/api"; import useLoading from "../hooks/useLoading"; import { workspaceService } from "../services"; import { useAppSelector } from "../store"; import { unknownWorkspace, unknownWorkspaceUser } from "../store/modules/workspace"; import { showCommonDialog } from "./Alert"; -import toastHelper from "./Toast"; import Icon from "./Icon"; import CreateWorkspaceDialog from "./CreateWorkspaceDialog"; import UpsertWorkspaceUserDialog from "./UpsertWorkspaceUserDialog"; @@ -38,7 +38,7 @@ const WorkspaceSetting: React.FC = (props: Props) => { useEffect(() => { const workspace = workspaceService.getWorkspaceById(workspaceId); if (!workspace) { - toastHelper.error("workspace not found"); + toast.error("workspace not found"); return; } diff --git a/web/src/css/toast.css b/web/src/css/toast.css deleted file mode 100644 index b65b18d..0000000 --- a/web/src/css/toast.css +++ /dev/null @@ -1,20 +0,0 @@ -.toast-list-container { - @apply flex flex-col justify-start items-end fixed top-2 right-4 max-h-full; - z-index: 99999; -} - -.toast-list-container > .toast-wrapper { - @apply flex flex-col justify-start items-start relative left-full invisible text-base cursor-pointer shadow-lg rounded bg-white mt-6 py-2 px-4; - min-width: 6em; - left: calc(100% + 32px); - transition: all 0.4s ease; -} - -.toast-list-container > .toast-wrapper.showup { - @apply left-0 visible; -} - -.toast-list-container > .toast-wrapper.destory { - @apply invisible; - left: calc(100% + 32px); -} diff --git a/web/src/pages/Auth.tsx b/web/src/pages/Auth.tsx index 30fcf38..6012f55 100644 --- a/web/src/pages/Auth.tsx +++ b/web/src/pages/Auth.tsx @@ -1,12 +1,12 @@ import { Button, Input } from "@mui/joy"; import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; +import { toast } from "react-hot-toast"; import * as api from "../helpers/api"; import { validate, ValidatorConfig } from "../helpers/validator"; import { userService } from "../services"; import useLoading from "../hooks/useLoading"; import Icon from "../components/Icon"; -import toastHelper from "../components/Toast"; const validateConfig: ValidatorConfig = { minLength: 4, @@ -53,13 +53,13 @@ const Auth: React.FC = () => { const emailValidResult = validate(email, validateConfig); if (!emailValidResult.result) { - toastHelper.error("Email: " + emailValidResult.reason); + toast.error("Email: " + emailValidResult.reason); return; } const passwordValidResult = validate(password, validateConfig); if (!passwordValidResult.result) { - toastHelper.error("Password: " + passwordValidResult.reason); + toast.error("Password: " + passwordValidResult.reason); return; } @@ -72,11 +72,11 @@ const Auth: React.FC = () => { replace: true, }); } else { - toastHelper.error("Signin failed"); + toast.error("Signin failed"); } } catch (error: any) { console.error(error); - toastHelper.error(JSON.stringify(error.response.data)); + toast.error(JSON.stringify(error.response.data)); } actionBtnLoadingState.setFinish(); }; @@ -88,13 +88,13 @@ const Auth: React.FC = () => { const emailValidResult = validate(email, validateConfig); if (!emailValidResult.result) { - toastHelper.error("Email: " + emailValidResult.reason); + toast.error("Email: " + emailValidResult.reason); return; } const passwordValidResult = validate(password, validateConfig); if (!passwordValidResult.result) { - toastHelper.error("Password: " + passwordValidResult.reason); + toast.error("Password: " + passwordValidResult.reason); return; } @@ -107,11 +107,11 @@ const Auth: React.FC = () => { replace: true, }); } else { - toastHelper.error("Signup failed"); + toast.error("Signup failed"); } } catch (error: any) { console.error(error); - toastHelper.error(JSON.stringify(error.response.data)); + toast.error(JSON.stringify(error.response.data)); } actionBtnLoadingState.setFinish(); }; diff --git a/web/src/pages/UserDetail.tsx b/web/src/pages/UserDetail.tsx index d91517a..c5070c5 100644 --- a/web/src/pages/UserDetail.tsx +++ b/web/src/pages/UserDetail.tsx @@ -1,12 +1,12 @@ import { Button, Input, Tooltip } from "@mui/joy"; import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; +import toast from "react-hot-toast"; import { useAppSelector } from "../store"; import { showCommonDialog } from "../components/Alert"; import { userService } from "../services"; import Icon from "../components/Icon"; import copy from "copy-to-clipboard"; -import toastHelper from "../components/Toast"; import ChangePasswordDialog from "../components/ChangePasswordDialog"; interface State { @@ -36,12 +36,12 @@ const UserDetail: React.FC = () => { const handleCopyOpenIdBtnClick = async () => { if (!user?.openId) { - toastHelper.error("OpenID not found"); + toast.error("OpenID not found"); return; } copy(user.openId); - toastHelper.success("OpenID copied"); + toast.success("OpenID copied"); }; const handleResetOpenIdBtnClick = async () => { diff --git a/web/src/pages/WorkspaceDetail.tsx b/web/src/pages/WorkspaceDetail.tsx index df8fcbf..4b68814 100644 --- a/web/src/pages/WorkspaceDetail.tsx +++ b/web/src/pages/WorkspaceDetail.tsx @@ -1,11 +1,11 @@ import { useEffect, useState } from "react"; import { NavLink, useLocation, useNavigate, useParams } from "react-router-dom"; +import toast from "react-hot-toast"; import { shortcutService, userService } from "../services"; import { useAppSelector } from "../store"; import { unknownWorkspace } from "../store/modules/workspace"; import useLoading from "../hooks/useLoading"; import Icon from "../components/Icon"; -import toastHelper from "../components/Toast"; import Dropdown from "../components/common/Dropdown"; import ShortcutListView from "../components/ShortcutListView"; import WorkspaceSetting from "../components/WorkspaceSetting"; @@ -34,7 +34,7 @@ const WorkspaceDetail: React.FC = () => { } if (!workspace) { - toastHelper.error("workspace not found"); + toast.error("workspace not found"); return; } diff --git a/web/yarn.lock b/web/yarn.lock index af511a2..b30b13d 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1495,6 +1495,11 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +goober@^2.1.10: + version "2.1.12" + resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.12.tgz#6c1645314ac9a68fe76408e1f502c63df8a39042" + integrity sha512-yXHAvO08FU1JgTXX6Zn6sYCUFfB/OJSX8HHjDSgerZHZmFKAb08cykp5LBw5QnmyMcZyPRMqkdyHUSSzge788Q== + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -2107,6 +2112,13 @@ react-dom@^18.1.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-hot-toast@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.4.0.tgz#b91e7a4c1b6e3068fc599d3d83b4fb48668ae51d" + integrity sha512-qnnVbXropKuwUpriVVosgo8QrB+IaPJCpL8oBI6Ov84uvHZ5QQcTp2qg6ku2wNfgJl6rlQXJIQU5q+5lmPOutA== + dependencies: + goober "^2.1.10" + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"