diff --git a/web/package.json b/web/package.json index f6adfb67..6830f5b6 100644 --- a/web/package.json +++ b/web/package.json @@ -23,6 +23,7 @@ "qs": "^6.11.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hot-toast": "^2.4.0", "react-i18next": "^11.18.6", "react-redux": "^8.0.1", "react-router-dom": "^6.8.2", diff --git a/web/src/App.tsx b/web/src/App.tsx index 28d83431..b5065ef3 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,5 +1,6 @@ import { useColorScheme } from "@mui/joy"; import { useEffect, Suspense } from "react"; +import { Toaster } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { RouterProvider } from "react-router-dom"; import router from "./router"; @@ -95,6 +96,7 @@ const App = () => { return ( }> + ); }; diff --git a/web/src/components/ArchivedMemo.tsx b/web/src/components/ArchivedMemo.tsx index 95d981f6..90c239df 100644 --- a/web/src/components/ArchivedMemo.tsx +++ b/web/src/components/ArchivedMemo.tsx @@ -1,8 +1,8 @@ +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useMemoStore } from "../store/module"; import * as utils from "../helpers/utils"; import useToggle from "../hooks/useToggle"; -import toastHelper from "./Toast"; import MemoContent from "./MemoContent"; import MemoResources from "./MemoResources"; import "../less/memo.less"; @@ -23,7 +23,7 @@ const ArchivedMemo: React.FC = (props: Props) => { await memoStore.deleteMemoById(memo.id); } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } } else { toggleConfirmDeleteBtn(); @@ -37,10 +37,10 @@ const ArchivedMemo: React.FC = (props: Props) => { rowStatus: "NORMAL", }); await memoStore.fetchMemos(); - toastHelper.info(t("message.restored-successfully")); + toast(t("message.restored-successfully")); } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } }; diff --git a/web/src/components/ArchivedMemoDialog.tsx b/web/src/components/ArchivedMemoDialog.tsx index c42a928c..5e484c73 100644 --- a/web/src/components/ArchivedMemoDialog.tsx +++ b/web/src/components/ArchivedMemoDialog.tsx @@ -1,10 +1,10 @@ import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useMemoStore } from "../store/module"; import useLoading from "../hooks/useLoading"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; -import toastHelper from "./Toast"; import ArchivedMemo from "./ArchivedMemo"; import "../less/archived-memo-dialog.less"; @@ -26,7 +26,7 @@ const ArchivedMemoDialog: React.FC = (props: Props) => { }) .catch((error) => { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); }) .finally(() => { loadingState.setFinish(); diff --git a/web/src/components/AskAIDialog.tsx b/web/src/components/AskAIDialog.tsx index aa86e412..fc1f17bd 100644 --- a/web/src/components/AskAIDialog.tsx +++ b/web/src/components/AskAIDialog.tsx @@ -1,11 +1,11 @@ import { Button, Textarea } from "@mui/joy"; import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import * as api from "../helpers/api"; import useLoading from "../hooks/useLoading"; import { marked } from "../labs/marked"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; -import toastHelper from "./Toast"; import showSettingDialog from "./SettingDialog"; type Props = DialogProps; @@ -44,7 +44,7 @@ const AskAIDialog: React.FC = (props: Props) => { await askQuestion(question); } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.error); + toast.error(error.response.data.error); } setQuestion(""); fetchingState.setFinish(); diff --git a/web/src/components/ChangeMemberPasswordDialog.tsx b/web/src/components/ChangeMemberPasswordDialog.tsx index d5befcf4..09ffadda 100644 --- a/web/src/components/ChangeMemberPasswordDialog.tsx +++ b/web/src/components/ChangeMemberPasswordDialog.tsx @@ -1,9 +1,9 @@ import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useUserStore } from "../store/module"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; -import toastHelper from "./Toast"; interface Props extends DialogProps { user: User; @@ -36,12 +36,12 @@ const ChangeMemberPasswordDialog: React.FC = (props: Props) => { const handleSaveBtnClick = async () => { if (newPassword === "" || newPasswordAgain === "") { - toastHelper.error(t("message.fill-all")); + toast.error(t("message.fill-all")); return; } if (newPassword !== newPasswordAgain) { - toastHelper.error(t("message.new-password-not-match")); + toast.error(t("message.new-password-not-match")); setNewPasswordAgain(""); return; } @@ -51,11 +51,11 @@ const ChangeMemberPasswordDialog: React.FC = (props: Props) => { id: propsUser.id, password: newPassword, }); - toastHelper.info(t("message.password-changed")); + toast(t("message.password-changed")); handleCloseBtnClick(); } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } }; diff --git a/web/src/components/ChangeMemoCreatedTsDialog.tsx b/web/src/components/ChangeMemoCreatedTsDialog.tsx index 124f9f52..1cdd0321 100644 --- a/web/src/components/ChangeMemoCreatedTsDialog.tsx +++ b/web/src/components/ChangeMemoCreatedTsDialog.tsx @@ -1,10 +1,10 @@ import dayjs from "dayjs"; import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useMemoStore } from "../store/module"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; -import toastHelper from "./Toast"; interface Props extends DialogProps { memoId: MemoId; @@ -23,7 +23,7 @@ const ChangeMemoCreatedTsDialog: React.FC = (props: Props) => { const datetime = dayjs(memo.createdTs).format("YYYY-MM-DDTHH:mm"); setCreatedAt(datetime); } else { - toastHelper.error(t("message.memo-not-found")); + toast.error(t("message.memo-not-found")); destroy(); } }); @@ -43,7 +43,7 @@ const ChangeMemoCreatedTsDialog: React.FC = (props: Props) => { const createdTs = dayjs(createdAt).unix(); if (createdTs > nowTs) { - toastHelper.error(t("message.invalid-created-datetime")); + toast.error(t("message.invalid-created-datetime")); return; } @@ -52,11 +52,11 @@ const ChangeMemoCreatedTsDialog: React.FC = (props: Props) => { id: memoId, createdTs, }); - toastHelper.info(t("message.memo-updated-datetime")); + toast.success(t("message.memo-updated-datetime")); handleCloseBtnClick(); } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } }; diff --git a/web/src/components/ChangePasswordDialog.tsx b/web/src/components/ChangePasswordDialog.tsx index b2839322..f8802f84 100644 --- a/web/src/components/ChangePasswordDialog.tsx +++ b/web/src/components/ChangePasswordDialog.tsx @@ -1,9 +1,9 @@ import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useUserStore } from "../store/module"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; -import toastHelper from "./Toast"; type Props = DialogProps; @@ -33,12 +33,12 @@ const ChangePasswordDialog: React.FC = ({ destroy }: Props) => { const handleSaveBtnClick = async () => { if (newPassword === "" || newPasswordAgain === "") { - toastHelper.error(t("message.fill-all")); + toast.error(t("message.fill-all")); return; } if (newPassword !== newPasswordAgain) { - toastHelper.error(t("message.new-password-not-match")); + toast.error(t("message.new-password-not-match")); setNewPasswordAgain(""); return; } @@ -49,11 +49,11 @@ const ChangePasswordDialog: React.FC = ({ destroy }: Props) => { id: user.id, password: newPassword, }); - toastHelper.info(t("message.password-changed")); + toast.success(t("message.password-changed")); handleCloseBtnClick(); } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } }; diff --git a/web/src/components/ChangeResourceFilenameDialog.tsx b/web/src/components/ChangeResourceFilenameDialog.tsx index c7720e73..065107c1 100644 --- a/web/src/components/ChangeResourceFilenameDialog.tsx +++ b/web/src/components/ChangeResourceFilenameDialog.tsx @@ -1,9 +1,9 @@ import { useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useResourceStore } from "../store/module"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; -import toastHelper from "./Toast"; import "../less/change-resource-filename-dialog.less"; interface Props extends DialogProps { @@ -44,7 +44,7 @@ const ChangeResourceFilenameDialog: React.FC = (props: Props) => { return; } if (!validateFilename(filename)) { - toastHelper.error(t("message.invalid-resource-filename")); + toast.error(t("message.invalid-resource-filename")); return; } try { @@ -52,11 +52,11 @@ const ChangeResourceFilenameDialog: React.FC = (props: Props) => { id: resourceId, filename: filename, }); - toastHelper.info(t("message.resource-filename-updated")); + toast.success(t("message.resource-filename-updated")); handleCloseBtnClick(); } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } }; diff --git a/web/src/components/CreateIdentityProviderDialog.tsx b/web/src/components/CreateIdentityProviderDialog.tsx index adc09bbb..beba6614 100644 --- a/web/src/components/CreateIdentityProviderDialog.tsx +++ b/web/src/components/CreateIdentityProviderDialog.tsx @@ -1,11 +1,11 @@ import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { Button, Divider, Input, Radio, RadioGroup, Typography } from "@mui/joy"; import * as api from "../helpers/api"; import { UNKNOWN_ID } from "../helpers/consts"; import { absolutifyLink } from "../helpers/utils"; import { generateDialog } from "./Dialog"; import Icon from "./Icon"; -import toastHelper from "./Toast"; interface Props extends DialogProps { identityProvider?: IdentityProvider; @@ -193,7 +193,7 @@ const CreateIdentityProviderDialog: React.FC = (props: Props) => { }, }, }); - toastHelper.info(`SSO ${basicInfo.name} created`); + toast.success(`SSO ${basicInfo.name} created`); } else { await api.patchIdentityProvider({ id: identityProvider?.id, @@ -206,11 +206,11 @@ const CreateIdentityProviderDialog: React.FC = (props: Props) => { }, }, }); - toastHelper.info(`SSO ${basicInfo.name} updated`); + toast.success(`SSO ${basicInfo.name} updated`); } } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } if (confirmCallback) { confirmCallback(); diff --git a/web/src/components/CreateResourceDialog.tsx b/web/src/components/CreateResourceDialog.tsx index 27e92bd7..9b809549 100644 --- a/web/src/components/CreateResourceDialog.tsx +++ b/web/src/components/CreateResourceDialog.tsx @@ -1,8 +1,8 @@ import { Button, Input, Select, Option, Typography, List, ListItem, Autocomplete, Tooltip } from "@mui/joy"; import React, { useRef, useState } from "react"; +import { toast } from "react-hot-toast"; import { useResourceStore } from "../store/module"; import Icon from "./Icon"; -import toastHelper from "./Toast"; import { generateDialog } from "./Dialog"; const fileTypeAutocompleteOptions = ["image/*", "text/*", "audio/*", "video/*", "application/*"]; @@ -132,7 +132,7 @@ const CreateResourceDialog: React.FC = (props: Props) => { } } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } if (onConfirm) { diff --git a/web/src/components/CreateShortcutDialog.tsx b/web/src/components/CreateShortcutDialog.tsx index 4507b684..f20bdc5e 100644 --- a/web/src/components/CreateShortcutDialog.tsx +++ b/web/src/components/CreateShortcutDialog.tsx @@ -1,12 +1,12 @@ import dayjs from "dayjs"; import { useCallback, useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useShortcutStore, useTagStore } from "../store/module"; import { filterConsts, getDefaultFilter, relationConsts } from "../helpers/filter"; import useLoading from "../hooks/useLoading"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; -import toastHelper from "./Toast"; import Selector from "./base/Selector"; import "../less/create-shortcut-dialog.less"; @@ -42,12 +42,12 @@ const CreateShortcutDialog: React.FC = (props: Props) => { const handleSaveBtnClick = async () => { if (!title) { - toastHelper.error(t("shortcut-list.title-required")); + toast.error(t("shortcut-list.title-required")); return; } for (const filter of filters) { if (!filter.value.value) { - toastHelper.error(t("shortcut-list.value-required")); + toast.error(t("shortcut-list.value-required")); return; } } @@ -66,7 +66,7 @@ const CreateShortcutDialog: React.FC = (props: Props) => { } } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } destroy(); }; @@ -75,7 +75,7 @@ const CreateShortcutDialog: React.FC = (props: Props) => { if (filters.length > 0) { const lastFilter = filters[filters.length - 1]; if (lastFilter.value.value === "") { - toastHelper.info(t("shortcut-list.fill-previous")); + toast(t("shortcut-list.fill-previous")); return; } } diff --git a/web/src/components/CreateStorageServiceDialog.tsx b/web/src/components/CreateStorageServiceDialog.tsx index 60b04afe..6cae3377 100644 --- a/web/src/components/CreateStorageServiceDialog.tsx +++ b/web/src/components/CreateStorageServiceDialog.tsx @@ -1,9 +1,9 @@ import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { Button, Input, Typography } from "@mui/joy"; import * as api from "../helpers/api"; import { generateDialog } from "./Dialog"; import Icon from "./Icon"; -import toastHelper from "./Toast"; interface Props extends DialogProps { storage?: ObjectStorage; @@ -77,7 +77,7 @@ const CreateStorageServiceDialog: React.FC = (props: Props) => { } } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } if (confirmCallback) { confirmCallback(); diff --git a/web/src/components/CreateTagDialog.tsx b/web/src/components/CreateTagDialog.tsx index 911ae698..27c0a9f0 100644 --- a/web/src/components/CreateTagDialog.tsx +++ b/web/src/components/CreateTagDialog.tsx @@ -1,11 +1,11 @@ import { Input } from "@mui/joy"; import React, { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTagStore } from "../store/module"; import { getTagSuggestionList } from "../helpers/api"; import { matcher } from "../labs/marked/matcher"; import Tag from "../labs/marked/parser/Tag"; import Icon from "./Icon"; -import toastHelper from "./Toast"; import { generateDialog } from "./Dialog"; type Props = DialogProps; @@ -54,7 +54,7 @@ const CreateTagDialog: React.FC = (props: Props) => { const handleSaveBtnClick = async () => { if (!validateTagName(tagName)) { - toastHelper.error("Invalid tag name"); + toast.error("Invalid tag name"); return; } @@ -63,7 +63,7 @@ const CreateTagDialog: React.FC = (props: Props) => { setTagName(""); } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } }; diff --git a/web/src/components/EmbedMemoDialog.tsx b/web/src/components/EmbedMemoDialog.tsx index aed984de..7947a438 100644 --- a/web/src/components/EmbedMemoDialog.tsx +++ b/web/src/components/EmbedMemoDialog.tsx @@ -1,8 +1,8 @@ import React from "react"; +import { toast } from "react-hot-toast"; +import copy from "copy-to-clipboard"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; -import copy from "copy-to-clipboard"; -import toastHelper from "./Toast"; interface Props extends DialogProps { memoId: MemoId; @@ -17,7 +17,7 @@ const EmbedMemoDialog: React.FC = (props: Props) => { const handleCopyCode = () => { copy(memoEmbeddedCode()); - toastHelper.success("Succeed to copy code to clipboard."); + toast.success("Succeed to copy code to clipboard."); }; return ( diff --git a/web/src/components/Memo.tsx b/web/src/components/Memo.tsx index 2b664cf0..dda7b244 100644 --- a/web/src/components/Memo.tsx +++ b/web/src/components/Memo.tsx @@ -2,11 +2,11 @@ import { Tooltip } from "@mui/joy"; import copy from "copy-to-clipboard"; import dayjs from "dayjs"; import { memo, useEffect, useRef, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { useEditorStore, useLocationStore, useMemoStore, useUserStore } from "../store/module"; import Icon from "./Icon"; -import toastHelper from "./Toast"; import MemoContent from "./MemoContent"; import MemoResources from "./MemoResources"; import showShareMemo from "./ShareMemoDialog"; @@ -64,7 +64,7 @@ const Memo: React.FC = (props: Props) => { const handleCopyContent = () => { copy(memo.content); - toastHelper.success(t("message.succeed-copy-content")); + toast.success(t("message.succeed-copy-content")); }; const handleTogglePinMemoBtnClick = async () => { @@ -103,7 +103,7 @@ const Memo: React.FC = (props: Props) => { }); } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } if (editorStore.getState().editMemoId === memo.id) { diff --git a/web/src/components/MemoEditor.tsx b/web/src/components/MemoEditor.tsx index f8741ec9..849f7fd2 100644 --- a/web/src/components/MemoEditor.tsx +++ b/web/src/components/MemoEditor.tsx @@ -1,5 +1,6 @@ import { isNumber, last, toLower, uniq } from "lodash-es"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { getMatchedNodes } from "../labs/marked"; import { deleteMemoResource, upsertMemoResource } from "../helpers/api"; @@ -15,7 +16,6 @@ import { } from "../store/module"; import * as storage from "../helpers/storage"; import Icon from "./Icon"; -import toastHelper from "./Toast"; import Selector from "./base/Selector"; import Editor, { EditorRefActions } from "./Editor/Editor"; import ResourceIcon from "./ResourceIcon"; @@ -214,7 +214,7 @@ const MemoEditor = () => { resource = await resourceStore.createResourceWithBlob(file); } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } setState((state) => { @@ -293,7 +293,7 @@ const MemoEditor = () => { } } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } setState((state) => { return { diff --git a/web/src/components/MemoList.tsx b/web/src/components/MemoList.tsx index ee177c81..3f932dbb 100644 --- a/web/src/components/MemoList.tsx +++ b/web/src/components/MemoList.tsx @@ -1,11 +1,11 @@ import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useLocationStore, useMemoStore, useShortcutStore } from "../store/module"; import { TAG_REG, LINK_REG } from "../labs/marked/parser"; import * as utils from "../helpers/utils"; import { DEFAULT_MEMO_LIMIT } from "../helpers/consts"; import { checkShouldShowMemoWithFilters } from "../helpers/filter"; -import toastHelper from "./Toast"; import Memo from "./Memo"; import "../less/memo-list.less"; @@ -95,7 +95,7 @@ const MemoList = () => { }) .catch((error) => { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); }); }, []); @@ -125,7 +125,7 @@ const MemoList = () => { } } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } }; diff --git a/web/src/components/ResourcesDialog.tsx b/web/src/components/ResourcesDialog.tsx index aa8a64df..244b6806 100644 --- a/web/src/components/ResourcesDialog.tsx +++ b/web/src/components/ResourcesDialog.tsx @@ -1,12 +1,12 @@ import { Button } from "@mui/joy"; import copy from "copy-to-clipboard"; import { useEffect } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import useLoading from "../hooks/useLoading"; import { useResourceStore } from "../store/module"; import { getResourceUrl } from "../utils/resource"; import Icon from "./Icon"; -import toastHelper from "./Toast"; import Dropdown from "./base/Dropdown"; import { generateDialog } from "./Dialog"; import { showCommonDialog } from "./Dialog/CommonDialog"; @@ -29,7 +29,7 @@ const ResourcesDialog: React.FC = (props: Props) => { .fetchResourceList() .catch((error) => { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); }) .finally(() => { loadingState.setFinish(); @@ -55,7 +55,7 @@ const ResourcesDialog: React.FC = (props: Props) => { const handleCopyResourceLinkBtnClick = (resource: Resource) => { const url = getResourceUrl(resource); copy(url); - toastHelper.success(t("message.succeed-copy-resource-link")); + toast.success(t("message.succeed-copy-resource-link")); }; const handleDeleteUnusedResourcesBtnClick = () => { @@ -68,7 +68,7 @@ const ResourcesDialog: React.FC = (props: Props) => { return false; }); if (unusedResources.length === 0) { - toastHelper.success(t("resources.no-unused-resources")); + toast.success(t("resources.no-unused-resources")); return; } showCommonDialog({ diff --git a/web/src/components/ResourcesSelectorDialog.tsx b/web/src/components/ResourcesSelectorDialog.tsx index 460c8df6..381fa5c3 100644 --- a/web/src/components/ResourcesSelectorDialog.tsx +++ b/web/src/components/ResourcesSelectorDialog.tsx @@ -1,11 +1,11 @@ import { Button, Checkbox } from "@mui/joy"; import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import useLoading from "../hooks/useLoading"; import { useEditorStore, useResourceStore } from "../store/module"; import { getResourceUrl } from "../utils/resource"; import Icon from "./Icon"; -import toastHelper from "./Toast"; import { generateDialog } from "./Dialog"; import showPreviewImageDialog from "./PreviewImageDialog"; import "../less/resources-selector-dialog.less"; @@ -32,7 +32,7 @@ const ResourcesSelectorDialog: React.FC = (props: Props) => { .fetchResourceList() .catch((error) => { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); }) .finally(() => { loadingState.setFinish(); diff --git a/web/src/components/Settings/MemberSection.tsx b/web/src/components/Settings/MemberSection.tsx index cf9d040f..9daa9157 100644 --- a/web/src/components/Settings/MemberSection.tsx +++ b/web/src/components/Settings/MemberSection.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useUserStore } from "../../store/module"; import * as api from "../../helpers/api"; -import toastHelper from "../Toast"; import Dropdown from "../base/Dropdown"; import { showCommonDialog } from "../Dialog/CommonDialog"; import showChangeMemberPasswordDialog from "../ChangeMemberPasswordDialog"; @@ -48,7 +48,7 @@ const PreferencesSection = () => { const handleCreateUserBtnClick = async () => { if (state.createUserUsername === "" || state.createUserPassword === "") { - toastHelper.error(t("message.fill-form")); + toast.error(t("message.fill-form")); return; } @@ -61,7 +61,7 @@ const PreferencesSection = () => { try { await api.createUser(userCreate); } catch (error: any) { - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } await fetchUserList(); setState({ diff --git a/web/src/components/Settings/SSOSection.tsx b/web/src/components/Settings/SSOSection.tsx index 91d381e3..26d7b4a5 100644 --- a/web/src/components/Settings/SSOSection.tsx +++ b/web/src/components/Settings/SSOSection.tsx @@ -1,10 +1,10 @@ import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import * as api from "../../helpers/api"; import showCreateIdentityProviderDialog from "../CreateIdentityProviderDialog"; import Dropdown from "../base/Dropdown"; import { showCommonDialog } from "../Dialog/CommonDialog"; -import toastHelper from "../Toast"; const SSOSection = () => { const { t } = useTranslation(); @@ -32,7 +32,7 @@ const SSOSection = () => { await api.deleteIdentityProvider(identityProvider.id); } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } await fetchIdentityProviderList(); }, diff --git a/web/src/components/Settings/StorageSection.tsx b/web/src/components/Settings/StorageSection.tsx index 78244416..239891ca 100644 --- a/web/src/components/Settings/StorageSection.tsx +++ b/web/src/components/Settings/StorageSection.tsx @@ -1,12 +1,12 @@ import { Divider, Select, Option } from "@mui/joy"; import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useGlobalStore } from "../../store/module"; import * as api from "../../helpers/api"; import showCreateStorageServiceDialog from "../CreateStorageServiceDialog"; import Dropdown from "../base/Dropdown"; import { showCommonDialog } from "../Dialog/CommonDialog"; -import toastHelper from "../Toast"; const StorageSection = () => { const { t } = useTranslation(); @@ -50,7 +50,7 @@ const StorageSection = () => { await api.deleteStorage(storage.id); } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } await fetchStorageList(); }, diff --git a/web/src/components/Settings/SystemSection.tsx b/web/src/components/Settings/SystemSection.tsx index 599f97dd..b3459ed5 100644 --- a/web/src/components/Settings/SystemSection.tsx +++ b/web/src/components/Settings/SystemSection.tsx @@ -1,9 +1,9 @@ import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { Button, Divider, Input, Switch, Textarea } from "@mui/joy"; import { useGlobalStore } from "../../store/module"; import * as api from "../../helpers/api"; -import toastHelper from "../Toast"; import showUpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog"; import { useAppDispatch } from "../../store"; import { setGlobalState } from "../../store/reducer/global"; @@ -83,7 +83,7 @@ const SystemSection = () => { console.error(error); return; } - toastHelper.success(t("message.succeed-vacuum-database")); + toast.success(t("message.succeed-vacuum-database")); }; const handleOpenAIApiKeyChanged = (value: string) => { @@ -103,7 +103,7 @@ const SystemSection = () => { console.error(error); return; } - toastHelper.success("OpenAI Api Key updated"); + toast.success("OpenAI Api Key updated"); }; const handleOpenAIApiHostChanged = (value: string) => { @@ -123,7 +123,7 @@ const SystemSection = () => { console.error(error); return; } - toastHelper.success("OpenAI Api Host updated"); + toast.success("OpenAI Api Host updated"); }; const handleAdditionalStyleChanged = (value: string) => { @@ -143,7 +143,7 @@ const SystemSection = () => { console.error(error); return; } - toastHelper.success(t("message.succeed-update-additional-style")); + toast.success(t("message.succeed-update-additional-style")); }; const handleAdditionalScriptChanged = (value: string) => { @@ -163,7 +163,7 @@ const SystemSection = () => { console.error(error); return; } - toastHelper.success(t("message.succeed-update-additional-script")); + toast.success(t("message.succeed-update-additional-script")); }; const handleDisablePublicMemosChanged = async (value: boolean) => { diff --git a/web/src/components/ShareMemoDialog.tsx b/web/src/components/ShareMemoDialog.tsx index 55063c9e..8c09917f 100644 --- a/web/src/components/ShareMemoDialog.tsx +++ b/web/src/components/ShareMemoDialog.tsx @@ -1,6 +1,7 @@ import { Select, Option } from "@mui/joy"; import { QRCodeSVG } from "qrcode.react"; import React, { useEffect, useRef, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import copy from "copy-to-clipboard"; import { toLower } from "lodash-es"; @@ -12,7 +13,6 @@ import { getMemoStats } from "../helpers/api"; import useLoading from "../hooks/useLoading"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; -import toastHelper from "./Toast"; import MemoContent from "./MemoContent"; import MemoResources from "./MemoResources"; import "../less/share-memo-dialog.less"; @@ -92,7 +92,7 @@ const ShareMemoDialog: React.FC = (props: Props) => { const handleCopyLinkBtnClick = () => { copy(`${window.location.origin}/m/${memo.id}`); - toastHelper.success(t("message.succeed-copy-link")); + toast.success(t("message.succeed-copy-link")); }; const memoVisibilityOptionSelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => { diff --git a/web/src/components/ShortcutList.tsx b/web/src/components/ShortcutList.tsx index 2175ac68..48816f8b 100644 --- a/web/src/components/ShortcutList.tsx +++ b/web/src/components/ShortcutList.tsx @@ -1,11 +1,11 @@ import { useEffect } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useLocationStore, useShortcutStore } from "../store/module"; import * as utils from "../helpers/utils"; import useToggle from "../hooks/useToggle"; import useLoading from "../hooks/useLoading"; import Icon from "./Icon"; -import toastHelper from "./Toast"; import showCreateShortcutDialog from "./CreateShortcutDialog"; const ShortcutList = () => { @@ -87,7 +87,7 @@ const ShortcutContainer: React.FC = (props: ShortcutCont } } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } } else { toggleConfirmDeleteBtn(); diff --git a/web/src/components/Toast.tsx b/web/src/components/Toast.tsx deleted file mode 100644 index 2b5ba9d0..00000000 --- a/web/src/components/Toast.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useEffect } from "react"; -import { createRoot, Root } from "react-dom/client"; -import "../less/toast.less"; - -type ToastType = "normal" | "success" | "info" | "error"; - -type ToastConfig = { - type: ToastType; - content: string; - duration: number; -}; - -type ToastItemProps = { - type: ToastType; - content: string; - duration: number; - destroy: FunctionType; -}; - -const Toast: React.FC = (props: ToastItemProps) => { - const { destroy, duration } = props; - - useEffect(() => { - if (duration > 0) { - setTimeout(() => { - destroy(); - }, 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 = { - destroy: () => { - tempDiv.classList.add("destroy"); - - 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 = 5000) => { - return showToast({ type: "error", content, duration }); - }; - - return { - info, - success, - error, - }; -}; - -const toastHelper = initialToastHelper(); - -export default toastHelper; diff --git a/web/src/components/UpdateAccountDialog.tsx b/web/src/components/UpdateAccountDialog.tsx index 9fc34927..311243e1 100644 --- a/web/src/components/UpdateAccountDialog.tsx +++ b/web/src/components/UpdateAccountDialog.tsx @@ -1,11 +1,11 @@ import { isEqual } from "lodash-es"; import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useUserStore } from "../store/module"; import { convertFileToBase64 } from "../helpers/utils"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; -import toastHelper from "./Toast"; import UserAvatar from "./UserAvatar"; type Props = DialogProps; @@ -41,7 +41,7 @@ const UpdateAccountDialog: React.FC = ({ destroy }: Props) => { if (files && files.length > 0) { const image = files[0]; if (image.size > 2 * 1024 * 1024) { - toastHelper.error("Max file size is 2MB"); + toast.error("Max file size is 2MB"); return; } try { @@ -54,7 +54,7 @@ const UpdateAccountDialog: React.FC = ({ destroy }: Props) => { }); } catch (error) { console.error(error); - toastHelper.error(`Failed to convert image to base64`); + toast.error(`Failed to convert image to base64`); } } }; @@ -88,7 +88,7 @@ const UpdateAccountDialog: React.FC = ({ destroy }: Props) => { const handleSaveBtnClick = async () => { if (state.username === "") { - toastHelper.error(t("message.fill-all")); + toast.error(t("message.fill-all")); return; } @@ -110,11 +110,11 @@ const UpdateAccountDialog: React.FC = ({ destroy }: Props) => { userPatch.email = state.email; } await userStore.patchUser(userPatch); - toastHelper.info(t("message.update-succeed")); + toast.success(t("message.update-succeed")); handleCloseBtnClick(); } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.error); + toast.error(error.response.data.error); } }; diff --git a/web/src/components/UpdateCustomizedProfileDialog.tsx b/web/src/components/UpdateCustomizedProfileDialog.tsx index b2bfd176..8f1bb66e 100644 --- a/web/src/components/UpdateCustomizedProfileDialog.tsx +++ b/web/src/components/UpdateCustomizedProfileDialog.tsx @@ -1,10 +1,10 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; +import { toast } from "react-hot-toast"; import { useGlobalStore } from "../store/module"; import * as api from "../helpers/api"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; -import toastHelper from "./Toast"; import LocaleSelect from "./LocaleSelect"; import AppearanceSelect from "./AppearanceSelect"; @@ -77,7 +77,7 @@ const UpdateCustomizedProfileDialog: React.FC = ({ destroy }: Props) => { const handleSaveButtonClick = async () => { if (state.name === "") { - toastHelper.error("Please fill server name"); + toast.error("Please fill server name"); return; } @@ -91,7 +91,7 @@ const UpdateCustomizedProfileDialog: React.FC = ({ destroy }: Props) => { console.error(error); return; } - toastHelper.success(t("message.succeed-update-customized-profile")); + toast.success(t("message.succeed-update-customized-profile")); destroy(); }; diff --git a/web/src/labs/marked/parser/CodeBlock.tsx b/web/src/labs/marked/parser/CodeBlock.tsx index 2f21ea11..1d522522 100644 --- a/web/src/labs/marked/parser/CodeBlock.tsx +++ b/web/src/labs/marked/parser/CodeBlock.tsx @@ -1,7 +1,7 @@ import copy from "copy-to-clipboard"; import hljs from "highlight.js"; +import { toast } from "react-hot-toast"; import { matcher } from "../matcher"; -import toastHelper from "../../../components/Toast"; export const CODE_BLOCK_REG = /^```(\S*?)\s([\s\S]*?)```/; @@ -25,7 +25,7 @@ const renderer = (rawStr: string) => { const handleCopyButtonClick = () => { copy(matchResult[2]); - toastHelper.success("Copy succeed"); + toast.success("Copy succeed"); }; return ( diff --git a/web/src/less/toast.less b/web/src/less/toast.less deleted file mode 100644 index 0d8b9ae1..00000000 --- a/web/src/less/toast.less +++ /dev/null @@ -1,26 +0,0 @@ -.toast-list-container { - @apply flex flex-col justify-start items-end fixed top-2 right-4 max-h-full; - z-index: 9999; - - > .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; - - &.showup { - @apply left-0 visible; - } - - &.destory { - @apply invisible; - left: calc(100% + 32px); - } - - > .toast-container { - > .content-text { - @apply text-sm whitespace-pre-wrap word-break leading-6 max-w-xs; - } - } - } -} diff --git a/web/src/pages/Auth.tsx b/web/src/pages/Auth.tsx index ca53e9d5..322cb70e 100644 --- a/web/src/pages/Auth.tsx +++ b/web/src/pages/Auth.tsx @@ -1,12 +1,12 @@ import { Button, Divider } from "@mui/joy"; import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useGlobalStore, useUserStore } from "../store/module"; import * as api from "../helpers/api"; import { absolutifyLink } from "../helpers/utils"; import useLoading from "../hooks/useLoading"; import Icon from "../components/Icon"; -import toastHelper from "../components/Toast"; import AppearanceSelect from "../components/AppearanceSelect"; import LocaleSelect from "../components/LocaleSelect"; import "../less/auth.less"; @@ -63,11 +63,11 @@ const Auth = () => { if (user) { window.location.href = "/"; } else { - toastHelper.error(t("message.login-failed")); + toast.error(t("message.login-failed")); } } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.error); + toast.error(error.response.data.error); } actionBtnLoadingState.setFinish(); }; @@ -84,11 +84,11 @@ const Auth = () => { if (user) { window.location.href = "/"; } else { - toastHelper.error(t("common.singup-failed")); + toast.error(t("common.singup-failed")); } } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.error); + toast.error(error.response.data.error); } actionBtnLoadingState.setFinish(); }; diff --git a/web/src/pages/AuthCallback.tsx b/web/src/pages/AuthCallback.tsx index 47a0d735..abf6f93a 100644 --- a/web/src/pages/AuthCallback.tsx +++ b/web/src/pages/AuthCallback.tsx @@ -1,9 +1,9 @@ import { last } from "lodash-es"; import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useSearchParams } from "react-router-dom"; import * as api from "../helpers/api"; -import toastHelper from "../components/Toast"; import { absolutifyLink } from "../helpers/utils"; import { useUserStore } from "../store/module"; import Icon from "../components/Icon"; @@ -41,7 +41,7 @@ const AuthCallback = () => { if (user) { window.location.href = "/"; } else { - toastHelper.error(t("message.login-failed")); + toast.error(t("message.login-failed")); } }) .catch((error: any) => { diff --git a/web/src/pages/EmbedMemo.tsx b/web/src/pages/EmbedMemo.tsx index fc789b4c..a07efbb7 100644 --- a/web/src/pages/EmbedMemo.tsx +++ b/web/src/pages/EmbedMemo.tsx @@ -1,11 +1,11 @@ import dayjs from "dayjs"; import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; import { UNKNOWN_ID } from "../helpers/consts"; import { useMemoStore } from "../store/module"; import useLoading from "../hooks/useLoading"; -import toastHelper from "../components/Toast"; import MemoContent from "../components/MemoContent"; import MemoResources from "../components/MemoResources"; @@ -36,8 +36,7 @@ const EmbedMemo = () => { loadingState.setFinish(); }) .catch((error) => { - console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); }); } }, []); diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index 3efd3351..e8e3e02e 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -1,11 +1,11 @@ import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { useGlobalStore, useLocationStore, useMemoStore, useUserStore } from "../store/module"; import { TAG_REG } from "../labs/marked/parser"; import { DEFAULT_MEMO_LIMIT } from "../helpers/consts"; import useLoading from "../hooks/useLoading"; -import toastHelper from "../components/Toast"; import Icon from "../components/Icon"; import MemoFilter from "../components/MemoFilter"; import Memo from "../components/Memo"; @@ -84,7 +84,7 @@ const Explore = () => { }); } catch (error: any) { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); } }; diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 21a6a37c..98f06b7a 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -1,8 +1,8 @@ import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useLocation } from "react-router-dom"; +import { toast } from "react-hot-toast"; import { useGlobalStore, useUserStore } from "../store/module"; -import toastHelper from "../components/Toast"; import MemoEditor from "../components/MemoEditor"; import MemoFilter from "../components/MemoFilter"; import MemoList from "../components/MemoList"; @@ -21,7 +21,7 @@ function Home() { if (userStore.isVisitorMode()) { if (!owner) { - toastHelper.error(t("message.user-not-found")); + toast.error(t("message.user-not-found")); } } }, [location]); diff --git a/web/src/pages/MemoDetail.tsx b/web/src/pages/MemoDetail.tsx index 2b5e5d2e..add01965 100644 --- a/web/src/pages/MemoDetail.tsx +++ b/web/src/pages/MemoDetail.tsx @@ -1,11 +1,11 @@ import dayjs from "dayjs"; import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { Link, useParams } from "react-router-dom"; import { UNKNOWN_ID } from "../helpers/consts"; import { useGlobalStore, useLocationStore, useMemoStore, useUserStore } from "../store/module"; import useLoading from "../hooks/useLoading"; -import toastHelper from "../components/Toast"; import MemoContent from "../components/MemoContent"; import MemoResources from "../components/MemoResources"; import "../less/memo-detail.less"; @@ -44,7 +44,7 @@ const MemoDetail = () => { }) .catch((error) => { console.error(error); - toastHelper.error(error.response.data.message); + toast.error(error.response.data.message); }); } }, [location]); diff --git a/web/yarn.lock b/web/yarn.lock index 494fbf4b..50f24c94 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1675,6 +1675,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== + gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -2505,6 +2510,13 @@ react-dom@^18.2.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-i18next@^11.18.6: version "11.18.6" resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.18.6.tgz#e159c2960c718c1314f1e8fcaa282d1c8b167887"