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 (
-
- );
-};
-
-// 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"