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