chore: add data empty placeholder (#1913)

This commit is contained in:
boojack 2023-07-08 13:04:12 +08:00 committed by GitHub
parent 7e391bd53d
commit 0292f472e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 170 additions and 443 deletions

BIN
web/public/assets/empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

View File

@ -3,10 +3,10 @@ import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useMemoStore } from "@/store/module"; import { useMemoStore } from "@/store/module";
import { getDateTimeString } from "@/helpers/datetime"; import { getDateTimeString } from "@/helpers/datetime";
import useToggle from "@/hooks/useToggle";
import Icon from "./Icon"; import Icon from "./Icon";
import MemoContent from "./MemoContent"; import MemoContent from "./MemoContent";
import MemoResourceListView from "./MemoResourceListView"; import MemoResourceListView from "./MemoResourceListView";
import { showCommonDialog } from "./Dialog/CommonDialog";
import "@/less/memo.less"; import "@/less/memo.less";
interface Props { interface Props {
@ -17,19 +17,17 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
const { memo } = props; const { memo } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const memoStore = useMemoStore(); const memoStore = useMemoStore();
const [showConfirmDeleteBtn, toggleConfirmDeleteBtn] = useToggle(false);
const handleDeleteMemoClick = async () => { const handleDeleteMemoClick = async () => {
if (showConfirmDeleteBtn) { showCommonDialog({
try { title: t("memo.delete-memo"),
content: t("memo.delete-confirm"),
style: "warning",
dialogName: "delete-memo-dialog",
onConfirm: async () => {
await memoStore.deleteMemoById(memo.id); await memoStore.deleteMemoById(memo.id);
} catch (error: any) { },
console.error(error); });
toast.error(error.response.data.message);
}
} else {
toggleConfirmDeleteBtn();
}
}; };
const handleRestoreMemoClick = async () => { const handleRestoreMemoClick = async () => {
@ -46,14 +44,8 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
} }
}; };
const handleMouseLeaveMemoWrapper = () => {
if (showConfirmDeleteBtn) {
toggleConfirmDeleteBtn(false);
}
};
return ( return (
<div className={`memo-wrapper archived ${"memos-" + memo.id}`} onMouseLeave={handleMouseLeaveMemoWrapper}> <div className={`memo-wrapper archived ${"memos-" + memo.id}`}>
<div className="memo-top-wrapper"> <div className="memo-top-wrapper">
<div className="status-text-container"> <div className="status-text-container">
<span className="time-text">{getDateTimeString(memo.updatedTs)}</span> <span className="time-text">{getDateTimeString(memo.updatedTs)}</span>
@ -65,10 +57,7 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
</button> </button>
</Tooltip> </Tooltip>
<Tooltip title={t("common.delete")} placement="top"> <Tooltip title={t("common.delete")} placement="top">
<button <button onClick={handleDeleteMemoClick} className="text-gray-500 dark:text-gray-400">
onClick={handleDeleteMemoClick}
className={`text-gray-500 dark:text-gray-400 ${showConfirmDeleteBtn ? "text-red-600" : ""}`}
>
<Icon.Trash className="w-4 h-auto cursor-pointer" /> <Icon.Trash className="w-4 h-auto cursor-pointer" />
</button> </button>
</Tooltip> </Tooltip>

View File

@ -1,12 +1,12 @@
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { Button, Divider, Input, Option, Select, Typography } from "@mui/joy"; import { Button, Divider, Input, Option, Select, Typography } from "@mui/joy";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "react-hot-toast";
import * as api from "@/helpers/api"; import * as api from "@/helpers/api";
import { UNKNOWN_ID } from "@/helpers/consts"; import { UNKNOWN_ID } from "@/helpers/consts";
import { absolutifyLink } from "@/helpers/utils"; import { absolutifyLink } from "@/helpers/utils";
import { generateDialog } from "./Dialog"; import { generateDialog } from "./Dialog";
import Icon from "./Icon"; import Icon from "./Icon";
import { useTranslation } from "react-i18next";
const templateList: IdentityProvider[] = [ const templateList: IdentityProvider[] = [
{ {
@ -170,7 +170,6 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
if (type === "OAUTH2") { if (type === "OAUTH2") {
if ( if (
oauth2Config.clientId === "" || oauth2Config.clientId === "" ||
oauth2Config.clientSecret === "" ||
oauth2Config.authUrl === "" || oauth2Config.authUrl === "" ||
oauth2Config.tokenUrl === "" || oauth2Config.tokenUrl === "" ||
oauth2Config.userInfoUrl === "" || oauth2Config.userInfoUrl === "" ||
@ -179,7 +178,13 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
) { ) {
return false; return false;
} }
if (isCreating) {
if (oauth2Config.clientSecret === "") {
return false;
} }
}
}
return true; return true;
}; };

View File

@ -6,7 +6,7 @@ import * as api from "@/helpers/api";
import { generateDialog } from "./Dialog"; import { generateDialog } from "./Dialog";
import Icon from "./Icon"; import Icon from "./Icon";
import RequiredBadge from "./RequiredBadge"; import RequiredBadge from "./RequiredBadge";
import HelpButton from "./kit/HelpButton"; import LearnMore from "./LearnMore";
interface Props extends DialogProps { interface Props extends DialogProps {
storage?: ObjectStorage; storage?: ObjectStorage;
@ -183,11 +183,9 @@ const CreateStorageServiceDialog: React.FC<Props> = (props: Props) => {
onChange={(e) => setPartialS3Config({ bucket: e.target.value })} onChange={(e) => setPartialS3Config({ bucket: e.target.value })}
fullWidth fullWidth
/> />
<div className="flex flex-row"> <div className="flex flex-row items-center mb-1">
<Typography className="!mb-1" level="body2"> <Typography level="body2">{t("setting.storage-section.path")}</Typography>
{t("setting.storage-section.path")} <LearnMore className="ml-1" title={t("setting.storage-section.path-description")} url="https://usememos.com/docs/local-storage" />
</Typography>
<HelpButton text={t("setting.storage-section.path-description")} url="https://usememos.com/docs/local-storage" />
</div> </div>
<Input <Input
className="mb-2" className="mb-2"

View File

@ -0,0 +1,9 @@
const Empty = () => {
return (
<div className="mx-auto">
<img className="w-24 h-auto dark:opacity-40" src="/assets/empty.png" alt="" />
</div>
);
};
export default Empty;

View File

@ -132,12 +132,12 @@ const Header = () => {
<Icon.Settings className="mr-3 w-6 h-auto opacity-70" /> {t("common.settings")} <Icon.Settings className="mr-3 w-6 h-auto opacity-70" /> {t("common.settings")}
</> </>
</NavLink> </NavLink>
<div className="pr-3 pl-1 w-full"> <div id="header-memo">
<button <button
className="mt-2 w-full py-3 rounded-full flex flex-row justify-center items-center bg-green-600 text-white hover:shadow hover:opacity-80" className="px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 bg-gray-50/100 dark:bg-zinc-700/20 hover:bg-white hover:shadow dark:hover:bg-zinc-700"
onClick={() => showMemoEditorDialog()} onClick={() => showMemoEditorDialog()}
> >
<Icon.Edit3 className="w-4 h-auto mr-1" /> <Icon.Feather className="mr-3 w-6 h-auto opacity-70" />
{t("common.new")} {t("common.new")}
</button> </button>
</div> </div>

View File

@ -21,12 +21,13 @@ import "@/less/memo.less";
interface Props { interface Props {
memo: Memo; memo: Memo;
readonly?: boolean; showCreator?: boolean;
showVisibility?: boolean;
showRelatedMemos?: boolean; showRelatedMemos?: boolean;
} }
const Memo: React.FC<Props> = (props: Props) => { const Memo: React.FC<Props> = (props: Props) => {
const { memo, readonly, showRelatedMemos } = props; const { memo, showCreator, showVisibility, showRelatedMemos } = props;
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const filterStore = useFilterStore(); const filterStore = useFilterStore();
const userStore = useUserStore(); const userStore = useUserStore();
@ -35,7 +36,7 @@ const Memo: React.FC<Props> = (props: Props) => {
const [createdTimeStr, setCreatedTimeStr] = useState<string>(getRelativeTimeString(memo.displayTs)); const [createdTimeStr, setCreatedTimeStr] = useState<string>(getRelativeTimeString(memo.displayTs));
const [relatedMemoList, setRelatedMemoList] = useState<Memo[]>([]); const [relatedMemoList, setRelatedMemoList] = useState<Memo[]>([]);
const memoContainerRef = useRef<HTMLDivElement>(null); const memoContainerRef = useRef<HTMLDivElement>(null);
const isVisitorMode = userStore.isVisitorMode() || readonly; const readonly = userStore.isVisitorMode() || userStore.getCurrentUserId() !== memo.creatorId;
useEffect(() => { useEffect(() => {
Promise.allSettled(memo.relationList.map((memoRelation) => memoCacheStore.getOrFetchMemoById(memoRelation.relatedMemoId))).then( Promise.allSettled(memo.relationList.map((memoRelation) => memoCacheStore.getOrFetchMemoById(memoRelation.relatedMemoId))).then(
@ -134,7 +135,7 @@ const Memo: React.FC<Props> = (props: Props) => {
filterStore.setTagFilter(tagName); filterStore.setTagFilter(tagName);
} }
} else if (targetEl.classList.contains("todo-block")) { } else if (targetEl.classList.contains("todo-block")) {
if (isVisitorMode) { if (readonly) {
return; return;
} }
@ -173,7 +174,7 @@ const Memo: React.FC<Props> = (props: Props) => {
}; };
const handleMemoContentDoubleClick = (e: React.MouseEvent) => { const handleMemoContentDoubleClick = (e: React.MouseEvent) => {
if (isVisitorMode) { if (readonly) {
return; return;
} }
@ -216,15 +217,14 @@ const Memo: React.FC<Props> = (props: Props) => {
<Link className="time-text" to={`/m/${memo.id}`} onClick={handleMemoCreatedTimeClick}> <Link className="time-text" to={`/m/${memo.id}`} onClick={handleMemoCreatedTimeClick}>
{createdTimeStr} {createdTimeStr}
</Link> </Link>
{isVisitorMode && ( {showCreator && (
<Link className="name-text" to={`/u/${memo.creatorId}`}> <Link className="name-text" to={`/u/${memo.creatorId}`}>
@{memo.creatorName} @{memo.creatorName}
</Link> </Link>
)} )}
</div> </div>
{!isVisitorMode && (
<div className="btns-container space-x-2"> <div className="btns-container space-x-2">
{memo.visibility !== "PRIVATE" && ( {showVisibility && memo.visibility !== "PRIVATE" && (
<Tooltip title={t(`memo.visibility.${memo.visibility.toLowerCase()}`)} placement="top"> <Tooltip title={t(`memo.visibility.${memo.visibility.toLowerCase()}`)} placement="top">
<div onClick={() => handleMemoVisibilityClick(memo.visibility)}> <div onClick={() => handleMemoVisibilityClick(memo.visibility)}>
{memo.visibility === "PUBLIC" ? ( {memo.visibility === "PUBLIC" ? (
@ -236,6 +236,8 @@ const Memo: React.FC<Props> = (props: Props) => {
</Tooltip> </Tooltip>
)} )}
{memo.pinned && <Icon.Bookmark className="w-4 h-auto rounded text-green-600" />} {memo.pinned && <Icon.Bookmark className="w-4 h-auto rounded text-green-600" />}
{!readonly && (
<>
<span className="btn more-action-btn"> <span className="btn more-action-btn">
<Icon.MoreHorizontal className="icon-img" /> <Icon.MoreHorizontal className="icon-img" />
</span> </span>
@ -268,9 +270,10 @@ const Memo: React.FC<Props> = (props: Props) => {
</span> </span>
</div> </div>
</div> </div>
</div> </>
)} )}
</div> </div>
</div>
<MemoContent <MemoContent
content={memo.content} content={memo.content}
onMemoContentClick={handleMemoContentClick} onMemoContentClick={handleMemoContentClick}
@ -282,14 +285,14 @@ const Memo: React.FC<Props> = (props: Props) => {
{showRelatedMemos && relatedMemoList.length > 0 && ( {showRelatedMemos && relatedMemoList.length > 0 && (
<> <>
<p className="text-sm dark:text-gray-300 mt-4 mb-1 pl-4 opacity-50 flex flex-row items-center"> <p className="text-sm dark:text-gray-300 my-2 pl-4 opacity-50 flex flex-row items-center">
<Icon.Link className="w-4 h-auto mr-1" /> <Icon.Link className="w-4 h-auto mr-1" />
<span>Related memos</span> <span>Related memos</span>
</p> </p>
{relatedMemoList.map((relatedMemo) => { {relatedMemoList.map((relatedMemo) => {
return ( return (
<div key={relatedMemo.id} className="w-full"> <div key={relatedMemo.id} className="w-full">
<Memo memo={relatedMemo} readonly={readonly} /> <Memo memo={relatedMemo} showCreator />
</div> </div>
); );
})} })}

View File

@ -24,14 +24,11 @@ interface State {
const MemoContent: React.FC<Props> = (props: Props) => { const MemoContent: React.FC<Props> = (props: Props) => {
const { className, content, showFull, onMemoContentClick, onMemoContentDoubleClick } = props; const { className, content, showFull, onMemoContentClick, onMemoContentDoubleClick } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const userStore = useUserStore();
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
expandButtonStatus: -1, expandButtonStatus: -1,
}); });
const memoContentContainerRef = useRef<HTMLDivElement>(null); const memoContentContainerRef = useRef<HTMLDivElement>(null);
//variable for auto-collapse
const userStore = useUserStore();
const isVisitorMode = userStore.isVisitorMode(); const isVisitorMode = userStore.isVisitorMode();
const autoCollapse: boolean = isVisitorMode ? true : (userStore.state.user as User).localSetting.enableAutoCollapse; const autoCollapse: boolean = isVisitorMode ? true : (userStore.state.user as User).localSetting.enableAutoCollapse;

View File

@ -2,13 +2,13 @@ import { useEffect, useRef, useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useFilterStore, useMemoStore, useShortcutStore, useUserStore } from "@/store/module"; import { useFilterStore, useMemoStore, useShortcutStore, useUserStore } from "@/store/module";
import { TAG_REG, LINK_REG } from "@/labs/marked/parser"; import { TAG_REG, LINK_REG, PLAIN_LINK_REG } from "@/labs/marked/parser";
import { getTimeStampByDate } from "@/helpers/datetime"; import { getTimeStampByDate } from "@/helpers/datetime";
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts"; import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
import { checkShouldShowMemoWithFilters } from "@/helpers/filter"; import { checkShouldShowMemoWithFilters } from "@/helpers/filter";
import Empty from "./Empty";
import Memo from "./Memo"; import Memo from "./Memo";
import "@/less/memo-list.less"; import "@/less/memo-list.less";
import { PLAIN_LINK_REG } from "@/labs/marked/parser";
const MemoList = () => { const MemoList = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -153,7 +153,7 @@ const MemoList = () => {
return ( return (
<div className="memo-list-container"> <div className="memo-list-container">
{sortedMemos.map((memo) => ( {sortedMemos.map((memo) => (
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} /> <Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} showVisibility />
))} ))}
{isFetching ? ( {isFetching ? (
<div className="status-text-container fetching-tip"> <div className="status-text-container fetching-tip">
@ -163,10 +163,11 @@ const MemoList = () => {
<div ref={statusRef} className="status-text-container"> <div ref={statusRef} className="status-text-container">
<p className="status-text"> <p className="status-text">
{isComplete ? ( {isComplete ? (
sortedMemos.length === 0 ? ( sortedMemos.length === 0 && (
t("message.no-memos") <div className="w-full mt-4 mb-8 flex flex-col justify-center items-center italic">
) : ( <Empty />
t("message.memos-ready") <p className="mt-4 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
</div>
) )
) : ( ) : (
<> <>

View File

@ -28,6 +28,8 @@ const MyAccountSection = () => {
}); });
}; };
const exampleWithCurl = `curl '${openAPIRoute}' -H 'Content-Type: application/json' --data-raw '{"content":"Hello world!"}'`;
return ( return (
<> <>
<div className="section-container account-section-container"> <div className="section-container account-section-container">
@ -48,22 +50,15 @@ const MyAccountSection = () => {
</div> </div>
</div> </div>
<div className="section-container openapi-section-container mt-6"> <div className="section-container openapi-section-container mt-6">
<p className="title-text">{t("setting.account-section.openapi-title")}</p> <p className="title-text">Open ID</p>
<Input className="w-full mb-3" value={openAPIRoute} readOnly /> <div className="w-full flex flex-row justify-start items-center">
<Button color="warning" className="mb-4" onClick={handleResetOpenIdBtnClick}> <Input className="grow mr-2" value={user.openId} readOnly />
{t("setting.account-section.reset-api")} <Icon.RefreshCw className="ml-2 h-4 w-4" /> <Button className="shrink-0" color="warning" onClick={handleResetOpenIdBtnClick}>
<Icon.RefreshCw className="h-4 w-4" />
</Button> </Button>
<Textarea </div>
className="w-full !font-mono !text-sm mt-4" <p className="title-text">Open API Example with cURL</p>
value={`POST ${openAPIRoute}\nContent-type: application/json\n{\n "content": "${t( <Textarea className="w-full !font-mono !text-sm whitespace-pre" value={exampleWithCurl} readOnly />
"setting.account-section.openapi-sample-post",
{
url: window.location.origin,
interpolation: { escapeValue: false },
}
)}"\n}`}
readOnly
/>
</div> </div>
</> </>
); );

View File

@ -7,7 +7,7 @@ import { useGlobalStore, useUserStore } from "@/store/module";
import { VISIBILITY_SELECTOR_ITEMS } from "@/helpers/consts"; import { VISIBILITY_SELECTOR_ITEMS } from "@/helpers/consts";
import AppearanceSelect from "../AppearanceSelect"; import AppearanceSelect from "../AppearanceSelect";
import LocaleSelect from "../LocaleSelect"; import LocaleSelect from "../LocaleSelect";
import HelpButton from "../kit/HelpButton"; import LearnMore from "../LearnMore";
import "@/less/settings/preferences-section.less"; import "@/less/settings/preferences-section.less";
const PreferencesSection = () => { const PreferencesSection = () => {
@ -141,7 +141,7 @@ const PreferencesSection = () => {
<div className="mb-2 w-full flex flex-row justify-between items-center"> <div className="mb-2 w-full flex flex-row justify-between items-center">
<div className="w-auto flex items-center"> <div className="w-auto flex items-center">
<span className="text-sm mr-1">{t("setting.preference-section.telegram-user-id")}</span> <span className="text-sm mr-1">{t("setting.preference-section.telegram-user-id")}</span>
<HelpButton icon="help" url="https://usememos.com/docs/integration/telegram-bot" /> <LearnMore url="https://usememos.com/docs/integration/telegram-bot" />
</div> </div>
<Button onClick={handleSaveTelegramUserId}>{t("common.save")}</Button> <Button onClick={handleSaveTelegramUserId}>{t("common.save")}</Button>
</div> </div>

View File

@ -1,12 +1,12 @@
import { Divider } from "@mui/joy";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import * as api from "@/helpers/api"; import * as api from "@/helpers/api";
import { Divider } from "@mui/joy";
import showCreateIdentityProviderDialog from "../CreateIdentityProviderDialog"; import showCreateIdentityProviderDialog from "../CreateIdentityProviderDialog";
import Dropdown from "../kit/Dropdown"; import Dropdown from "../kit/Dropdown";
import { showCommonDialog } from "../Dialog/CommonDialog"; import { showCommonDialog } from "../Dialog/CommonDialog";
import HelpButton from "../kit/HelpButton"; import LearnMore from "../LearnMore";
const SSOSection = () => { const SSOSection = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -43,7 +43,7 @@ const SSOSection = () => {
<div className="section-container"> <div className="section-container">
<div className="mb-2 w-full flex flex-row justify-start items-center gap-1"> <div className="mb-2 w-full flex flex-row justify-start items-center gap-1">
<span className="font-mono text-sm text-gray-400">{t("setting.sso-section.sso-list")}</span> <span className="font-mono text-sm text-gray-400">{t("setting.sso-section.sso-list")}</span>
<HelpButton icon="help" url="https://usememos.com/docs/keycloak" /> <LearnMore url="https://usememos.com/docs/keycloak" />
<button <button
className="btn-normal px-2 py-0 ml-1" className="btn-normal px-2 py-0 ml-1"
onClick={() => showCreateIdentityProviderDialog(undefined, fetchIdentityProviderList)} onClick={() => showCreateIdentityProviderDialog(undefined, fetchIdentityProviderList)}
@ -57,7 +57,7 @@ const SSOSection = () => {
{identityProviderList.map((identityProvider) => ( {identityProviderList.map((identityProvider) => (
<div <div
key={identityProvider.id} key={identityProvider.id}
className="py-2 w-full border-t last:border-b dark:border-zinc-700 flex flex-row items-center justify-between" className="py-2 w-full border-b last:border-b dark:border-zinc-700 flex flex-row items-center justify-between"
> >
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
<p className="ml-2"> <p className="ml-2">

View File

@ -8,7 +8,7 @@ import showCreateStorageServiceDialog from "../CreateStorageServiceDialog";
import showUpdateLocalStorageDialog from "../UpdateLocalStorageDialog"; import showUpdateLocalStorageDialog from "../UpdateLocalStorageDialog";
import Dropdown from "../kit/Dropdown"; import Dropdown from "../kit/Dropdown";
import { showCommonDialog } from "../Dialog/CommonDialog"; import { showCommonDialog } from "../Dialog/CommonDialog";
import HelpButton from "../kit/HelpButton"; import LearnMore from "../LearnMore";
const StorageSection = () => { const StorageSection = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -76,7 +76,7 @@ const StorageSection = () => {
<Divider /> <Divider />
<div className="mt-4 mb-2 w-full flex flex-row justify-start items-center gap-1"> <div className="mt-4 mb-2 w-full flex flex-row justify-start items-center gap-1">
<span className="font-mono text-sm text-gray-400">{t("setting.storage-section.storage-services-list")}</span> <span className="font-mono text-sm text-gray-400">{t("setting.storage-section.storage-services-list")}</span>
<HelpButton className="btn" icon="info" url="https://usememos.com/docs/storage" /> <LearnMore url="https://usememos.com/docs/storage" />
<button className="btn-normal px-2 py-0 ml-1" onClick={() => showCreateStorageServiceDialog(undefined, fetchStorageList)}> <button className="btn-normal px-2 py-0 ml-1" onClick={() => showCreateStorageServiceDialog(undefined, fetchStorageList)}>
{t("common.create")} {t("common.create")}
</button> </button>

View File

@ -1,12 +1,13 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button, Divider, Input, Switch, Textarea } from "@mui/joy"; import { Button, Divider, Input, Switch, Textarea, Tooltip } from "@mui/joy";
import { formatBytes } from "@/helpers/utils"; import { formatBytes } from "@/helpers/utils";
import { useGlobalStore } from "@/store/module"; import { useGlobalStore } from "@/store/module";
import * as api from "@/helpers/api"; import * as api from "@/helpers/api";
import HelpButton from "../kit/HelpButton";
import showUpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog"; import showUpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog";
import Icon from "../Icon";
import LearnMore from "../LearnMore";
import "@/less/settings/system-section.less"; import "@/less/settings/system-section.less";
interface State { interface State {
@ -224,7 +225,9 @@ const SystemSection = () => {
<div className="form-label"> <div className="form-label">
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
<span className="text-sm mr-1">{t("setting.system-section.max-upload-size")}</span> <span className="text-sm mr-1">{t("setting.system-section.max-upload-size")}</span>
<HelpButton icon="info" hint={t("setting.system-section.max-upload-size-hint")} /> <Tooltip title={t("setting.system-section.max-upload-size-hint")} placement="top">
<Icon.HelpCircle className="w-4 h-auto" />
</Tooltip>
</div> </div>
<Input <Input
className="w-16" className="w-16"
@ -241,9 +244,9 @@ const SystemSection = () => {
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
<div className="w-auto flex items-center"> <div className="w-auto flex items-center">
<span className="text-sm mr-1">{t("setting.system-section.telegram-bot-token")}</span> <span className="text-sm mr-1">{t("setting.system-section.telegram-bot-token")}</span>
<HelpButton <LearnMore
hint={t("setting.system-section.telegram-bot-token-description")}
url="https://usememos.com/docs/integration/telegram-bot" url="https://usememos.com/docs/integration/telegram-bot"
title={t("setting.system-section.telegram-bot-token-description")}
/> />
</div> </div>
</div> </div>

View File

@ -40,7 +40,7 @@ const UserBanner = () => {
<Dropdown <Dropdown
className="w-full" className="w-full"
trigger={ trigger={
<div className="px-3 py-2 max-w-full flex flex-row justify-start items-center cursor-pointer rounded-lg hover:shadow hover:bg-white dark:hover:bg-zinc-700"> <div className="px-4 py-2 max-w-full flex flex-row justify-start items-center cursor-pointer rounded-full hover:shadow hover:bg-white dark:hover:bg-zinc-700">
<UserAvatar avatarUrl={user?.avatarUrl} /> <UserAvatar avatarUrl={user?.avatarUrl} />
<span className="px-1 text-lg font-medium text-slate-800 dark:text-gray-200 shrink truncate"> <span className="px-1 text-lg font-medium text-slate-800 dark:text-gray-200 shrink truncate">
{userStore.isVisitorMode() ? systemStatus.customizedProfile.name : username} {userStore.isVisitorMode() ? systemStatus.customizedProfile.name : username}

View File

@ -1,283 +0,0 @@
import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { Button, IconButton, Tooltip } from "@mui/joy";
import { generateDialog } from "../Dialog";
import Icon from "../Icon";
const openUrl = (url?: string) => {
window.open(url, "_blank");
};
/** Options for {@link HelpButton} */
interface HelpProps {
/**
* Plain text to show in the dialog.
*
* If the text contains "\n", it will be split to multiple paragraphs.
*/
text?: string;
/**
* The title of the dialog.
*
* If not provided, the title will be set according to the `icon` prop.
*/
title?: string;
/**
* External documentation URL.
*
* If provided, this will be shown as a link button in the bottom of the dialog.
*
* If provided alone, the button will just open the URL in a new tab.
*
* @param {string} url - External URL to the documentation.
*/
url?: string;
/**
* The tooltip of the button.
*/
hint?: string | "none";
/**
* The placement of the hovering hint.
* @defaultValue "top"
*/
hintPlacement?: "top" | "bottom" | "left" | "right";
/**
* The icon to show in the button.
*
* Also used to infer `title` and `hint`, if they are not provided.
*
* @defaultValue Icon.HelpCircle
* @see {@link Icon.LucideIcon}
*/
icon?: Icon.LucideIcon | "link" | "info" | "help" | "alert" | "warn";
/**
* The className for the button.
* @defaultValue `!-mt-2` (aligns the button vertically with nearby text)
*/
className?: string;
/**
* The color of the button.
* @defaultValue "neutral"
*/
color?: "primary" | "neutral" | "danger" | "info" | "success" | "warning";
/**
* The variant of the button.
* @defaultValue "plain"
*/
variant?: "plain" | "outlined" | "soft" | "solid";
/**
* The size of the button.
* @defaultValue "md"
*/
size?: "sm" | "md" | "lg";
/**
* `ReactNode` HTML content to show in the dialog.
*
* If provided, will be shown before `text`.
*
* You'll probably want to use `text` instead.
*/
children?: ReactNode | undefined;
}
interface HelpDialogProps extends HelpProps, DialogProps {}
const HelpfulDialog: React.FC<HelpDialogProps> = (props: HelpDialogProps) => {
const { t } = useTranslation();
const { children, destroy, icon } = props;
const LucideIcon = icon as Icon.LucideIcon;
const handleCloseBtnClick = () => {
destroy();
};
return (
<>
<div className="dialog-header-container">
<LucideIcon size="24" />
<p className="title-text text-left">{props.title}</p>
<button className="btn close-btn" onClick={handleCloseBtnClick}>
<Icon.X />
</button>
</div>
<div className="dialog-content-container max-w-sm">
{children}
{props.text
? props.text.split(/\n|\\n/).map((text) => {
return (
<p key={text} className="mt-2 break-words text-justify">
{text}
</p>
);
})
: null}
<div className="mt-2 w-full flex flex-row justify-end space-x-2">
{props.url ? (
<Button className="btn-normal" variant="outlined" color={props.color} onClick={() => openUrl(props.url)}>
{t("common.learn-more")}
<Icon.ExternalLink className="ml-1 w-4 h-4 opacity-80" />
</Button>
) : null}
<Button className="btn-normal" variant="outlined" color={props.color} onClick={handleCloseBtnClick}>
{t("common.close")}
</Button>
</div>
</div>
</>
);
};
function showHelpDialog(props: HelpProps) {
generateDialog(
{
className: "help-dialog",
dialogName: "help-dialog",
clickSpaceDestroy: true,
},
HelpfulDialog,
props
);
}
/**
* Show a helpful `IconButton` that behaves differently depending on the props.
*
* The main purpose of this component is to avoid UI clutter.
*
* Use the property `icon` to set the icon and infer the title and hint automatically.
*
* Use cases:
* - Button with just a hover hint
* - Button with a hover hint and link
* - Button with a hover hint that opens a dialog with text and a link.
*
* @example
* <Helpful hint="Hint" />
* <Helpful hint="This is a hint with a link" url="https://usememos.com/" />
* <Helpful icon="warn" text={t("i18n.key.long-dialog-text")} url="https://usememos.com/" />
* <Helpful />
*
* <div className="flex flex-row">
* <span className="ml-2">Sample alignment</span>
* <Helpful hint="Button with hint" />
* </div>
* @param props.title - The title of the dialog. Defaults to "Learn more" i18n key.
* @param props.text - Plain text to show in the dialog. Line breaks are supported.
* @param props.url - External memos documentation URL.
* @param props.hint - The hint when hovering the button.
* @param props.hintPlacement - The placement of the hovering hint. Defaults to "top".
* @param props.icon - The icon to show in the button.
* @param props.className - The class name for the button.
* @param {HelpProps} props - See {@link HelpDialogProps} for all exposed props.
*/
const HelpButton = (props: HelpProps): JSX.Element => {
const { t } = useTranslation();
const color = props.color ?? "neutral";
const variant = props.variant ?? "plain";
const className = props.className ?? "";
const hintPlacement = props.hintPlacement ?? "top";
const iconButtonSize = "sm";
const dialogAvailable = props.text || props.children;
const clickActionAvailable = props.url || dialogAvailable;
const onlyUrlAvailable = props.url && !dialogAvailable;
let LucideIcon = (() => {
switch (props.icon) {
case "info":
return Icon.Info;
case "help":
return Icon.HelpCircle;
case "warn":
case "alert":
return Icon.AlertTriangle;
case "link":
return Icon.ExternalLink;
default:
return Icon.HelpCircle;
}
})() as Icon.LucideIcon;
const hint = (() => {
switch (props.hint) {
case undefined:
return t(
(() => {
if (!dialogAvailable) {
LucideIcon = Icon.ExternalLink;
}
switch (LucideIcon) {
case Icon.Info:
return "common.dialog.info";
case Icon.AlertTriangle:
return "common.dialog.warning";
case Icon.ExternalLink:
return "common.learn-more";
case Icon.HelpCircle:
default:
return "common.dialog.help";
}
})()
);
case "":
case "none":
case "false":
case "disabled":
return undefined;
default:
return props.hint;
}
})();
const sizePx = (() => {
switch (props.size) {
case "sm":
return 14;
case "lg":
return 18;
case "md":
default:
return 16;
}
})();
if (!dialogAvailable && !clickActionAvailable && !props.hint) {
return (
<IconButton className={className} color={color} variant={variant} size={iconButtonSize}>
<LucideIcon size={sizePx} />
</IconButton>
);
}
const wrapInTooltip = (element: JSX.Element) => {
if (!hint) {
return element;
}
return (
<Tooltip placement={hintPlacement} title={hint} color={color} variant={variant} size={props.size}>
{element}
</Tooltip>
);
};
if (clickActionAvailable) {
props = { ...props, title: props.title ?? hint, hint: hint, color: color, variant: variant, icon: LucideIcon };
const clickAction = () => {
dialogAvailable ? showHelpDialog(props) : openUrl(props.url);
};
LucideIcon = dialogAvailable || onlyUrlAvailable ? LucideIcon : Icon.ExternalLink;
return wrapInTooltip(
<IconButton className={className} color={color} variant={variant} size={iconButtonSize} onClick={clickAction}>
<LucideIcon size={sizePx} />
</IconButton>
);
}
return wrapInTooltip(
<IconButton className={className} color={color} variant={variant} size={iconButtonSize}>
<LucideIcon size={sizePx} />
</IconButton>
);
};
export default HelpButton;

View File

@ -1,5 +1,5 @@
.filter-query-container { .filter-query-container {
@apply flex flex-row justify-start items-start w-full flex-wrap px-2 pt-2 text-sm font-mono leading-7 dark:text-gray-300; @apply flex flex-row justify-start items-start w-full flex-wrap px-2 pb-2 text-sm font-mono leading-7 dark:text-gray-300;
> .filter-item-container { > .filter-item-container {
@apply flex flex-row justify-start items-center px-2 mr-2 cursor-pointer dark:text-gray-300 bg-gray-200 dark:bg-zinc-700 rounded whitespace-nowrap truncate hover:line-through; @apply flex flex-row justify-start items-center px-2 mr-2 cursor-pointer dark:text-gray-300 bg-gray-200 dark:bg-zinc-700 rounded whitespace-nowrap truncate hover:line-through;

View File

@ -1,5 +1,5 @@
.memo-wrapper { .memo-wrapper {
@apply relative flex flex-col justify-start items-start w-full p-4 pt-3 mt-2 bg-white dark:bg-zinc-700 rounded-lg border border-white dark:border-zinc-600 hover:border-gray-200 dark:hover:border-zinc-600; @apply relative flex flex-col justify-start items-start w-full p-4 pt-3 mb-2 bg-white dark:bg-zinc-700 rounded-lg border border-white dark:border-zinc-600 hover:border-gray-200 dark:hover:border-zinc-600;
&.pinned { &.pinned {
@apply border-gray-200 border-2 dark:border-zinc-600; @apply border-gray-200 border-2 dark:border-zinc-600;

View File

@ -26,7 +26,7 @@
} }
> .section-content-container { > .section-content-container {
@apply w-full sm:w-auto pl-2 grow flex flex-col justify-start items-start h-full; @apply w-full sm:w-auto pl-2 pb-4 grow flex flex-col justify-start items-start h-full;
> .section-container { > .section-container {
@apply flex flex-col justify-start items-start w-full; @apply flex flex-col justify-start items-start w-full;

View File

@ -324,9 +324,8 @@
"or": "Or" "or": "Or"
}, },
"message": { "message": {
"no-memos": "no memos 🌃", "no-data": "Maybe no data was found, maybe it should be another option.",
"memos-ready": "all memos are ready 🎉", "memos-ready": "all memos are ready 🎉",
"no-resource": "no resource 🌃",
"resource-ready": "all resource are ready 🎉", "resource-ready": "all resource are ready 🎉",
"restored-successfully": "Restored successfully", "restored-successfully": "Restored successfully",
"memo-updated-datetime": "Memo created datetime changed.", "memo-updated-datetime": "Memo created datetime changed.",

View File

@ -225,8 +225,7 @@
"memo-updated-datetime": "备忘录创建日期时间已更改。", "memo-updated-datetime": "备忘录创建日期时间已更改。",
"memos-ready": "所有备忘录已就绪 🎉", "memos-ready": "所有备忘录已就绪 🎉",
"new-password-not-match": "新密码不匹配。", "new-password-not-match": "新密码不匹配。",
"no-memos": "没有备忘录了 🌃", "no-data": "也许寻觅无果,也许可重新设定标准。",
"no-resource": "没有资源了 🌃",
"not-allow-chinese": "不允许包含中文", "not-allow-chinese": "不允许包含中文",
"not-allow-space": "不允许包含空格", "not-allow-space": "不允许包含空格",
"page-not-found": "404 - 未找到网页 😥", "page-not-found": "404 - 未找到网页 😥",

View File

@ -339,9 +339,8 @@
"or": "或" "or": "或"
}, },
"message": { "message": {
"no-memos": "沒有 Memo 了 🌃", "no-data": "或許尋覓虛空,或者改換選擇之軌跡。",
"memos-ready": "所有 Memo 已就緒 🎉", "memos-ready": "所有 Memo 已就緒 🎉",
"no-resource": "沒有 Resource 了 🌃",
"resource-ready": "所有 Resource 已就緒 🎉", "resource-ready": "所有 Resource 已就緒 🎉",
"restored-successfully": "還原成功", "restored-successfully": "還原成功",
"memo-updated-datetime": "Memo 建立日期時間已變更。", "memo-updated-datetime": "Memo 建立日期時間已變更。",

View File

@ -5,6 +5,7 @@ import { useMemoStore } from "@/store/module";
import useLoading from "@/hooks/useLoading"; import useLoading from "@/hooks/useLoading";
import ArchivedMemo from "@/components/ArchivedMemo"; import ArchivedMemo from "@/components/ArchivedMemo";
import MobileHeader from "@/components/MobileHeader"; import MobileHeader from "@/components/MobileHeader";
import Empty from "@/components/Empty";
import "@/less/archived.less"; import "@/less/archived.less";
const Archived = () => { const Archived = () => {
@ -30,7 +31,7 @@ const Archived = () => {
}, [memos]); }, [memos]);
return ( return (
<section className="w-full min-h-full flex flex-col md:flex-row justify-start items-start px-4 sm:px-2 pt-2 pb-8 bg-zinc-100 dark:bg-zinc-800"> <section className="w-full min-h-full flex flex-col md:flex-row justify-start items-start px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
<MobileHeader showSearch={false} /> <MobileHeader showSearch={false} />
<div className="archived-memo-page"> <div className="archived-memo-page">
{loadingState.isLoading ? ( {loadingState.isLoading ? (
@ -38,8 +39,9 @@ const Archived = () => {
<p className="tip-text">{t("memo.fetching-data")}</p> <p className="tip-text">{t("memo.fetching-data")}</p>
</div> </div>
) : archivedMemos.length === 0 ? ( ) : archivedMemos.length === 0 ? (
<div className="tip-text-container"> <div className="w-full mt-16 mb-8 flex flex-col justify-center items-center italic">
<p className="tip-text">{t("memo.no-archived-memos")}</p> <Empty />
<p className="mt-4 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
</div> </div>
) : ( ) : (
<div className="archived-memos-container"> <div className="archived-memos-container">

View File

@ -14,6 +14,7 @@ import DailyMemo from "@/components/DailyMemo";
import i18n from "@/i18n"; import i18n from "@/i18n";
import { findNearestLanguageMatch } from "@/utils/i18n"; import { findNearestLanguageMatch } from "@/utils/i18n";
import { convertToMillis, getDateStampByDate, getNormalizedDateString, getTimeStampByDate } from "@/helpers/datetime"; import { convertToMillis, getDateStampByDate, getNormalizedDateString, getTimeStampByDate } from "@/helpers/datetime";
import Empty from "@/components/Empty";
const DailyReview = () => { const DailyReview = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -117,7 +118,7 @@ const DailyReview = () => {
</button> </button>
</div> </div>
<DatePicker <DatePicker
className={`absolute top-8 mt-2 z-20 mx-auto border bg-white dark:bg-zinc-800 dark:border-zinc-800 rounded-lg mb-6 ${ className={`absolute top-8 mt-2 z-20 mx-auto border bg-white shadow dark:bg-zinc-800 dark:border-zinc-800 rounded-lg mb-6 ${
showDatePicker ? "" : "!hidden" showDatePicker ? "" : "!hidden"
}`} }`}
datestamp={currentDateStamp} datestamp={currentDateStamp}
@ -141,8 +142,9 @@ const DailyReview = () => {
</div> </div>
</div> </div>
{dailyMemos.length === 0 ? ( {dailyMemos.length === 0 ? (
<div className="mx-auto pt-4 pb-5 px-0"> <div className="w-full mt-4 mb-8 flex flex-col justify-center items-center italic">
<p className="italic text-gray-400">{t("daily-review.no-memos")}</p> <Empty />
<p className="mt-4 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
</div> </div>
) : ( ) : (
<div className="flex flex-col justify-start items-start w-full mt-2"> <div className="flex flex-col justify-start items-start w-full mt-2">

View File

@ -9,6 +9,7 @@ import useLoading from "@/hooks/useLoading";
import MemoFilter from "@/components/MemoFilter"; import MemoFilter from "@/components/MemoFilter";
import Memo from "@/components/Memo"; import Memo from "@/components/Memo";
import MobileHeader from "@/components/MobileHeader"; import MobileHeader from "@/components/MobileHeader";
import Empty from "@/components/Empty";
interface State { interface State {
memos: Memo[]; memos: Memo[];
@ -66,6 +67,7 @@ const Explore = () => {
: state.memos; : state.memos;
const sortedMemos = shownMemos.filter((m) => m.rowStatus === "NORMAL"); const sortedMemos = shownMemos.filter((m) => m.rowStatus === "NORMAL");
const handleFetchMoreClick = async () => { const handleFetchMoreClick = async () => {
try { try {
const fetchedMemos = await memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, state.memos.length); const fetchedMemos = await memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, state.memos.length);
@ -87,15 +89,18 @@ const Explore = () => {
<section className="w-full max-w-3xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800"> <section className="w-full max-w-3xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
<MobileHeader showSearch={false} /> <MobileHeader showSearch={false} />
{!loadingState.isLoading && ( {!loadingState.isLoading && (
<main className="relative w-full h-auto flex flex-col justify-start items-start -mt-2"> <main className="relative w-full h-auto flex flex-col justify-start items-start">
<MemoFilter /> <MemoFilter />
{sortedMemos.map((memo) => { {sortedMemos.map((memo) => {
return <Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} readonly={true} />; return <Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} showCreator />;
})} })}
{isComplete ? ( {isComplete ? (
state.memos.length === 0 ? ( state.memos.length === 0 && (
<p className="w-full text-center mt-12 text-gray-600">{t("message.no-memos")}</p> <div className="w-full mt-16 mb-8 flex flex-col justify-center items-center italic">
) : null <Empty />
<p className="mt-4 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
</div>
)
) : ( ) : (
<p className="m-auto text-center mt-4 italic cursor-pointer text-gray-500 hover:text-green-600" onClick={handleFetchMoreClick}> <p className="m-auto text-center mt-4 italic cursor-pointer text-gray-500 hover:text-green-600" onClick={handleFetchMoreClick}>
{t("memo.fetch-more")} {t("memo.fetch-more")}

View File

@ -35,7 +35,7 @@ function Home() {
<div className="flex-grow shrink w-auto px-4 sm:px-2 sm:pt-4"> <div className="flex-grow shrink w-auto px-4 sm:px-2 sm:pt-4">
<MobileHeader /> <MobileHeader />
<div className="w-full h-auto flex flex-col justify-start items-start bg-zinc-100 dark:bg-zinc-800 rounded-lg"> <div className="w-full h-auto flex flex-col justify-start items-start bg-zinc-100 dark:bg-zinc-800 rounded-lg">
{!userStore.isVisitorMode() && <MemoEditor />} {!userStore.isVisitorMode() && <MemoEditor className="mb-2" />}
<MemoFilter /> <MemoFilter />
</div> </div>
<MemoList /> <MemoList />

View File

@ -56,7 +56,7 @@ const MemoDetail = () => {
{!loadingState.isLoading && ( {!loadingState.isLoading && (
<> <>
<main className="relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4"> <main className="relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4">
<Memo memo={state.memo} readonly showRelatedMemos /> <Memo memo={state.memo} showCreator showRelatedMemos />
</main> </main>
<div className="mt-4 w-full flex flex-row justify-center items-center gap-2"> <div className="mt-4 w-full flex flex-row justify-center items-center gap-2">
<Link <Link

View File

@ -4,6 +4,7 @@ import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts"; import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
import useLoading from "@/hooks/useLoading"; import useLoading from "@/hooks/useLoading";
import useEvent from "@/hooks/useEvent";
import { useResourceStore } from "@/store/module"; import { useResourceStore } from "@/store/module";
import Icon from "@/components/Icon"; import Icon from "@/components/Icon";
import ResourceCard from "@/components/ResourceCard"; import ResourceCard from "@/components/ResourceCard";
@ -13,7 +14,7 @@ import Dropdown from "@/components/kit/Dropdown";
import ResourceItem from "@/components/ResourceItem"; import ResourceItem from "@/components/ResourceItem";
import { showCommonDialog } from "@/components/Dialog/CommonDialog"; import { showCommonDialog } from "@/components/Dialog/CommonDialog";
import showCreateResourceDialog from "@/components/CreateResourceDialog"; import showCreateResourceDialog from "@/components/CreateResourceDialog";
import useEvent from "@/hooks/useEvent"; import Empty from "@/components/Empty";
const ResourcesDashboard = () => { const ResourcesDashboard = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -297,7 +298,10 @@ const ResourcesDashboard = () => {
</div> </div>
)} )}
{resourceList.length === 0 ? ( {resourceList.length === 0 ? (
<p className="w-full text-center text-base my-6 mt-8">{t("resource.no-resources")}</p> <div className="w-full mt-8 mb-8 flex flex-col justify-center items-center italic">
<Empty />
<p className="mt-4 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
</div>
) : ( ) : (
resourceList resourceList
)} )}

View File

@ -15,9 +15,9 @@ const defaultSetting: Setting = {
}; };
const defaultLocalSetting: LocalSetting = { const defaultLocalSetting: LocalSetting = {
enableDoubleClickEditing: true, enableDoubleClickEditing: false,
enableAutoCollapse: false,
dailyReviewTimeOffset: 0, dailyReviewTimeOffset: 0,
enableAutoCollapse: true,
}; };
export const convertResponseModelUser = (user: User): User => { export const convertResponseModelUser = (user: User): User => {

View File

@ -13,8 +13,8 @@ interface Setting {
interface LocalSetting { interface LocalSetting {
enableDoubleClickEditing: boolean; enableDoubleClickEditing: boolean;
dailyReviewTimeOffset: number;
enableAutoCollapse: boolean; enableAutoCollapse: boolean;
dailyReviewTimeOffset: number;
} }
interface UserLocaleSetting { interface UserLocaleSetting {