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 { useMemoStore } from "@/store/module";
import { getDateTimeString } from "@/helpers/datetime";
import useToggle from "@/hooks/useToggle";
import Icon from "./Icon";
import MemoContent from "./MemoContent";
import MemoResourceListView from "./MemoResourceListView";
import { showCommonDialog } from "./Dialog/CommonDialog";
import "@/less/memo.less";
interface Props {
@ -17,19 +17,17 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
const { memo } = props;
const { t } = useTranslation();
const memoStore = useMemoStore();
const [showConfirmDeleteBtn, toggleConfirmDeleteBtn] = useToggle(false);
const handleDeleteMemoClick = async () => {
if (showConfirmDeleteBtn) {
try {
showCommonDialog({
title: t("memo.delete-memo"),
content: t("memo.delete-confirm"),
style: "warning",
dialogName: "delete-memo-dialog",
onConfirm: async () => {
await memoStore.deleteMemoById(memo.id);
} catch (error: any) {
console.error(error);
toast.error(error.response.data.message);
}
} else {
toggleConfirmDeleteBtn();
}
},
});
};
const handleRestoreMemoClick = async () => {
@ -46,14 +44,8 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
}
};
const handleMouseLeaveMemoWrapper = () => {
if (showConfirmDeleteBtn) {
toggleConfirmDeleteBtn(false);
}
};
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="status-text-container">
<span className="time-text">{getDateTimeString(memo.updatedTs)}</span>
@ -65,10 +57,7 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
</button>
</Tooltip>
<Tooltip title={t("common.delete")} placement="top">
<button
onClick={handleDeleteMemoClick}
className={`text-gray-500 dark:text-gray-400 ${showConfirmDeleteBtn ? "text-red-600" : ""}`}
>
<button onClick={handleDeleteMemoClick} className="text-gray-500 dark:text-gray-400">
<Icon.Trash className="w-4 h-auto cursor-pointer" />
</button>
</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 { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "react-hot-toast";
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 { useTranslation } from "react-i18next";
const templateList: IdentityProvider[] = [
{
@ -170,7 +170,6 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
if (type === "OAUTH2") {
if (
oauth2Config.clientId === "" ||
oauth2Config.clientSecret === "" ||
oauth2Config.authUrl === "" ||
oauth2Config.tokenUrl === "" ||
oauth2Config.userInfoUrl === "" ||
@ -179,7 +178,13 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
) {
return false;
}
if (isCreating) {
if (oauth2Config.clientSecret === "") {
return false;
}
}
}
return true;
};

View File

@ -6,7 +6,7 @@ import * as api from "@/helpers/api";
import { generateDialog } from "./Dialog";
import Icon from "./Icon";
import RequiredBadge from "./RequiredBadge";
import HelpButton from "./kit/HelpButton";
import LearnMore from "./LearnMore";
interface Props extends DialogProps {
storage?: ObjectStorage;
@ -183,11 +183,9 @@ const CreateStorageServiceDialog: React.FC<Props> = (props: Props) => {
onChange={(e) => setPartialS3Config({ bucket: e.target.value })}
fullWidth
/>
<div className="flex flex-row">
<Typography className="!mb-1" level="body2">
{t("setting.storage-section.path")}
</Typography>
<HelpButton text={t("setting.storage-section.path-description")} url="https://usememos.com/docs/local-storage" />
<div className="flex flex-row items-center mb-1">
<Typography level="body2">{t("setting.storage-section.path")}</Typography>
<LearnMore className="ml-1" title={t("setting.storage-section.path-description")} url="https://usememos.com/docs/local-storage" />
</div>
<Input
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")}
</>
</NavLink>
<div className="pr-3 pl-1 w-full">
<div id="header-memo">
<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()}
>
<Icon.Edit3 className="w-4 h-auto mr-1" />
<Icon.Feather className="mr-3 w-6 h-auto opacity-70" />
{t("common.new")}
</button>
</div>

View File

@ -21,12 +21,13 @@ import "@/less/memo.less";
interface Props {
memo: Memo;
readonly?: boolean;
showCreator?: boolean;
showVisibility?: boolean;
showRelatedMemos?: boolean;
}
const Memo: React.FC<Props> = (props: Props) => {
const { memo, readonly, showRelatedMemos } = props;
const { memo, showCreator, showVisibility, showRelatedMemos } = props;
const { t, i18n } = useTranslation();
const filterStore = useFilterStore();
const userStore = useUserStore();
@ -35,7 +36,7 @@ const Memo: React.FC<Props> = (props: Props) => {
const [createdTimeStr, setCreatedTimeStr] = useState<string>(getRelativeTimeString(memo.displayTs));
const [relatedMemoList, setRelatedMemoList] = useState<Memo[]>([]);
const memoContainerRef = useRef<HTMLDivElement>(null);
const isVisitorMode = userStore.isVisitorMode() || readonly;
const readonly = userStore.isVisitorMode() || userStore.getCurrentUserId() !== memo.creatorId;
useEffect(() => {
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);
}
} else if (targetEl.classList.contains("todo-block")) {
if (isVisitorMode) {
if (readonly) {
return;
}
@ -173,7 +174,7 @@ const Memo: React.FC<Props> = (props: Props) => {
};
const handleMemoContentDoubleClick = (e: React.MouseEvent) => {
if (isVisitorMode) {
if (readonly) {
return;
}
@ -216,60 +217,62 @@ const Memo: React.FC<Props> = (props: Props) => {
<Link className="time-text" to={`/m/${memo.id}`} onClick={handleMemoCreatedTimeClick}>
{createdTimeStr}
</Link>
{isVisitorMode && (
{showCreator && (
<Link className="name-text" to={`/u/${memo.creatorId}`}>
@{memo.creatorName}
</Link>
)}
</div>
{!isVisitorMode && (
<div className="btns-container space-x-2">
{memo.visibility !== "PRIVATE" && (
<Tooltip title={t(`memo.visibility.${memo.visibility.toLowerCase()}`)} placement="top">
<div onClick={() => handleMemoVisibilityClick(memo.visibility)}>
{memo.visibility === "PUBLIC" ? (
<Icon.Globe2 className="w-4 h-auto cursor-pointer rounded text-green-600" />
) : (
<Icon.Users className="w-4 h-auto cursor-pointer rounded text-gray-500 dark:text-gray-400" />
)}
</div>
</Tooltip>
)}
{memo.pinned && <Icon.Bookmark className="w-4 h-auto rounded text-green-600" />}
<span className="btn more-action-btn">
<Icon.MoreHorizontal className="icon-img" />
</span>
<div className="more-action-btns-wrapper">
<div className="more-action-btns-container min-w-[6em]">
<span className="btn" onClick={handleTogglePinMemoBtnClick}>
{memo.pinned ? <Icon.BookmarkMinus className="w-4 h-auto mr-2" /> : <Icon.BookmarkPlus className="w-4 h-auto mr-2" />}
{memo.pinned ? t("common.unpin") : t("common.pin")}
</span>
<span className="btn" onClick={handleEditMemoClick}>
<Icon.Edit3 className="w-4 h-auto mr-2" />
{t("common.edit")}
</span>
<span className="btn" onClick={handleGenerateMemoImageBtnClick}>
<Icon.Share className="w-4 h-auto mr-2" />
{t("common.share")}
</span>
<span className="btn" onClick={handleMarkMemoClick}>
<Icon.Link className="w-4 h-auto mr-2" />
Mark
</span>
<Divider className="!my-1" />
<span className="btn text-orange-500" onClick={handleArchiveMemoClick}>
<Icon.Archive className="w-4 h-auto mr-2" />
{t("common.archive")}
</span>
<span className="btn text-red-600" onClick={handleDeleteMemoClick}>
<Icon.Trash className="w-4 h-auto mr-2" />
{t("common.delete")}
</span>
<div className="btns-container space-x-2">
{showVisibility && memo.visibility !== "PRIVATE" && (
<Tooltip title={t(`memo.visibility.${memo.visibility.toLowerCase()}`)} placement="top">
<div onClick={() => handleMemoVisibilityClick(memo.visibility)}>
{memo.visibility === "PUBLIC" ? (
<Icon.Globe2 className="w-4 h-auto cursor-pointer rounded text-green-600" />
) : (
<Icon.Users className="w-4 h-auto cursor-pointer rounded text-gray-500 dark:text-gray-400" />
)}
</div>
</div>
</div>
)}
</Tooltip>
)}
{memo.pinned && <Icon.Bookmark className="w-4 h-auto rounded text-green-600" />}
{!readonly && (
<>
<span className="btn more-action-btn">
<Icon.MoreHorizontal className="icon-img" />
</span>
<div className="more-action-btns-wrapper">
<div className="more-action-btns-container min-w-[6em]">
<span className="btn" onClick={handleTogglePinMemoBtnClick}>
{memo.pinned ? <Icon.BookmarkMinus className="w-4 h-auto mr-2" /> : <Icon.BookmarkPlus className="w-4 h-auto mr-2" />}
{memo.pinned ? t("common.unpin") : t("common.pin")}
</span>
<span className="btn" onClick={handleEditMemoClick}>
<Icon.Edit3 className="w-4 h-auto mr-2" />
{t("common.edit")}
</span>
<span className="btn" onClick={handleGenerateMemoImageBtnClick}>
<Icon.Share className="w-4 h-auto mr-2" />
{t("common.share")}
</span>
<span className="btn" onClick={handleMarkMemoClick}>
<Icon.Link className="w-4 h-auto mr-2" />
Mark
</span>
<Divider className="!my-1" />
<span className="btn text-orange-500" onClick={handleArchiveMemoClick}>
<Icon.Archive className="w-4 h-auto mr-2" />
{t("common.archive")}
</span>
<span className="btn text-red-600" onClick={handleDeleteMemoClick}>
<Icon.Trash className="w-4 h-auto mr-2" />
{t("common.delete")}
</span>
</div>
</div>
</>
)}
</div>
</div>
<MemoContent
content={memo.content}
@ -282,14 +285,14 @@ const Memo: React.FC<Props> = (props: Props) => {
{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" />
<span>Related memos</span>
</p>
{relatedMemoList.map((relatedMemo) => {
return (
<div key={relatedMemo.id} className="w-full">
<Memo memo={relatedMemo} readonly={readonly} />
<Memo memo={relatedMemo} showCreator />
</div>
);
})}

View File

@ -24,14 +24,11 @@ interface State {
const MemoContent: React.FC<Props> = (props: Props) => {
const { className, content, showFull, onMemoContentClick, onMemoContentDoubleClick } = props;
const { t } = useTranslation();
const userStore = useUserStore();
const [state, setState] = useState<State>({
expandButtonStatus: -1,
});
const memoContentContainerRef = useRef<HTMLDivElement>(null);
//variable for auto-collapse
const userStore = useUserStore();
const isVisitorMode = userStore.isVisitorMode();
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 { useTranslation } from "react-i18next";
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 { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
import { checkShouldShowMemoWithFilters } from "@/helpers/filter";
import Empty from "./Empty";
import Memo from "./Memo";
import "@/less/memo-list.less";
import { PLAIN_LINK_REG } from "@/labs/marked/parser";
const MemoList = () => {
const { t } = useTranslation();
@ -153,7 +153,7 @@ const MemoList = () => {
return (
<div className="memo-list-container">
{sortedMemos.map((memo) => (
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} />
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} showVisibility />
))}
{isFetching ? (
<div className="status-text-container fetching-tip">
@ -163,10 +163,11 @@ const MemoList = () => {
<div ref={statusRef} className="status-text-container">
<p className="status-text">
{isComplete ? (
sortedMemos.length === 0 ? (
t("message.no-memos")
) : (
t("message.memos-ready")
sortedMemos.length === 0 && (
<div className="w-full mt-4 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>
)
) : (
<>

View File

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

View File

@ -7,7 +7,7 @@ import { useGlobalStore, useUserStore } from "@/store/module";
import { VISIBILITY_SELECTOR_ITEMS } from "@/helpers/consts";
import AppearanceSelect from "../AppearanceSelect";
import LocaleSelect from "../LocaleSelect";
import HelpButton from "../kit/HelpButton";
import LearnMore from "../LearnMore";
import "@/less/settings/preferences-section.less";
const PreferencesSection = () => {
@ -141,7 +141,7 @@ const PreferencesSection = () => {
<div className="mb-2 w-full flex flex-row justify-between items-center">
<div className="w-auto flex items-center">
<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>
<Button onClick={handleSaveTelegramUserId}>{t("common.save")}</Button>
</div>

View File

@ -1,12 +1,12 @@
import { Divider } from "@mui/joy";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import * as api from "@/helpers/api";
import { Divider } from "@mui/joy";
import showCreateIdentityProviderDialog from "../CreateIdentityProviderDialog";
import Dropdown from "../kit/Dropdown";
import { showCommonDialog } from "../Dialog/CommonDialog";
import HelpButton from "../kit/HelpButton";
import LearnMore from "../LearnMore";
const SSOSection = () => {
const { t } = useTranslation();
@ -43,7 +43,7 @@ const SSOSection = () => {
<div className="section-container">
<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>
<HelpButton icon="help" url="https://usememos.com/docs/keycloak" />
<LearnMore url="https://usememos.com/docs/keycloak" />
<button
className="btn-normal px-2 py-0 ml-1"
onClick={() => showCreateIdentityProviderDialog(undefined, fetchIdentityProviderList)}
@ -57,7 +57,7 @@ const SSOSection = () => {
{identityProviderList.map((identityProvider) => (
<div
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">
<p className="ml-2">

View File

@ -8,7 +8,7 @@ import showCreateStorageServiceDialog from "../CreateStorageServiceDialog";
import showUpdateLocalStorageDialog from "../UpdateLocalStorageDialog";
import Dropdown from "../kit/Dropdown";
import { showCommonDialog } from "../Dialog/CommonDialog";
import HelpButton from "../kit/HelpButton";
import LearnMore from "../LearnMore";
const StorageSection = () => {
const { t } = useTranslation();
@ -76,7 +76,7 @@ const StorageSection = () => {
<Divider />
<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>
<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)}>
{t("common.create")}
</button>

View File

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

View File

@ -40,7 +40,7 @@ const UserBanner = () => {
<Dropdown
className="w-full"
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} />
<span className="px-1 text-lg font-medium text-slate-800 dark:text-gray-200 shrink truncate">
{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 {
@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 {
@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 {
@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 {
@apply border-gray-200 border-2 dark:border-zinc-600;

View File

@ -26,7 +26,7 @@
}
> .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 {
@apply flex flex-col justify-start items-start w-full;

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import { useMemoStore } from "@/store/module";
import useLoading from "@/hooks/useLoading";
import ArchivedMemo from "@/components/ArchivedMemo";
import MobileHeader from "@/components/MobileHeader";
import Empty from "@/components/Empty";
import "@/less/archived.less";
const Archived = () => {
@ -30,7 +31,7 @@ const Archived = () => {
}, [memos]);
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} />
<div className="archived-memo-page">
{loadingState.isLoading ? (
@ -38,8 +39,9 @@ const Archived = () => {
<p className="tip-text">{t("memo.fetching-data")}</p>
</div>
) : archivedMemos.length === 0 ? (
<div className="tip-text-container">
<p className="tip-text">{t("memo.no-archived-memos")}</p>
<div className="w-full mt-16 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>
) : (
<div className="archived-memos-container">

View File

@ -14,6 +14,7 @@ import DailyMemo from "@/components/DailyMemo";
import i18n from "@/i18n";
import { findNearestLanguageMatch } from "@/utils/i18n";
import { convertToMillis, getDateStampByDate, getNormalizedDateString, getTimeStampByDate } from "@/helpers/datetime";
import Empty from "@/components/Empty";
const DailyReview = () => {
const { t } = useTranslation();
@ -117,7 +118,7 @@ const DailyReview = () => {
</button>
</div>
<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"
}`}
datestamp={currentDateStamp}
@ -141,8 +142,9 @@ const DailyReview = () => {
</div>
</div>
{dailyMemos.length === 0 ? (
<div className="mx-auto pt-4 pb-5 px-0">
<p className="italic text-gray-400">{t("daily-review.no-memos")}</p>
<div className="w-full mt-4 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>
) : (
<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 Memo from "@/components/Memo";
import MobileHeader from "@/components/MobileHeader";
import Empty from "@/components/Empty";
interface State {
memos: Memo[];
@ -66,6 +67,7 @@ const Explore = () => {
: state.memos;
const sortedMemos = shownMemos.filter((m) => m.rowStatus === "NORMAL");
const handleFetchMoreClick = async () => {
try {
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">
<MobileHeader showSearch={false} />
{!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 />
{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 ? (
state.memos.length === 0 ? (
<p className="w-full text-center mt-12 text-gray-600">{t("message.no-memos")}</p>
) : null
state.memos.length === 0 && (
<div className="w-full mt-16 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>
)
) : (
<p className="m-auto text-center mt-4 italic cursor-pointer text-gray-500 hover:text-green-600" onClick={handleFetchMoreClick}>
{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">
<MobileHeader />
<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 />
</div>
<MemoList />

View File

@ -56,7 +56,7 @@ const MemoDetail = () => {
{!loadingState.isLoading && (
<>
<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>
<div className="mt-4 w-full flex flex-row justify-center items-center gap-2">
<Link

View File

@ -4,6 +4,7 @@ import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
import useLoading from "@/hooks/useLoading";
import useEvent from "@/hooks/useEvent";
import { useResourceStore } from "@/store/module";
import Icon from "@/components/Icon";
import ResourceCard from "@/components/ResourceCard";
@ -13,7 +14,7 @@ import Dropdown from "@/components/kit/Dropdown";
import ResourceItem from "@/components/ResourceItem";
import { showCommonDialog } from "@/components/Dialog/CommonDialog";
import showCreateResourceDialog from "@/components/CreateResourceDialog";
import useEvent from "@/hooks/useEvent";
import Empty from "@/components/Empty";
const ResourcesDashboard = () => {
const { t } = useTranslation();
@ -297,7 +298,10 @@ const ResourcesDashboard = () => {
</div>
)}
{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
)}

View File

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

View File

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