mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: update memo editor
This commit is contained in:
@ -18,11 +18,11 @@ const getCellAdditionalStyles = (count: number, maxCount: number) => {
|
||||
|
||||
const ratio = count / maxCount;
|
||||
if (ratio > 0.7) {
|
||||
return "bg-teal-600 text-gray-100 dark:opacity-80";
|
||||
return "bg-teal-700 text-gray-100 dark:opacity-80";
|
||||
} else if (ratio > 0.4) {
|
||||
return "bg-teal-400 text-gray-100 dark:opacity-80";
|
||||
return "bg-teal-600 text-gray-100 dark:opacity-80";
|
||||
} else {
|
||||
return "bg-teal-300 text-gray-100 dark:opacity-80";
|
||||
return "bg-teal-500 text-gray-100 dark:opacity-70";
|
||||
}
|
||||
};
|
||||
|
||||
@ -58,29 +58,36 @@ const ActivityCalendar = (props: Props) => {
|
||||
const tooltipText = count ? t("memo.count-memos-in-date", { count: count, date: date }) : date;
|
||||
const isSelected = new Date(props.selectedDate).toDateString() === new Date(date).toDateString();
|
||||
return day ? (
|
||||
<Tooltip className="shrink-0" key={`${date}-${index}`} title={tooltipText} placement="top" arrow>
|
||||
count > 0 ? (
|
||||
<Tooltip className="shrink-0" key={`${date}-${index}`} title={tooltipText} placement="top" arrow>
|
||||
<div
|
||||
className={clsx(
|
||||
"w-6 h-6 text-xs rounded-xl flex justify-center items-center border cursor-default",
|
||||
getCellAdditionalStyles(count, maxCount),
|
||||
isToday && "border-zinc-400 dark:border-zinc-300",
|
||||
isSelected && "font-bold border-zinc-400 dark:border-zinc-300",
|
||||
!isToday && !isSelected && "border-transparent",
|
||||
)}
|
||||
onClick={() => count && onClick && onClick(new Date(date).toDateString())}
|
||||
>
|
||||
{day}
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div
|
||||
key={`${date}-${index}`}
|
||||
className={clsx(
|
||||
"w-6 h-6 text-xs rounded-xl flex justify-center items-center border",
|
||||
getCellAdditionalStyles(count, maxCount),
|
||||
isToday && "border-gray-600 dark:border-zinc-300",
|
||||
isSelected && "font-bold border-gray-600 dark:border-zinc-300",
|
||||
"w-6 h-6 text-xs rounded-xl flex justify-center items-center border cursor-default",
|
||||
"bg-gray-100 text-gray-400 dark:bg-zinc-800 dark:text-gray-500",
|
||||
isToday && "border-zinc-400 dark:border-zinc-500",
|
||||
!isToday && !isSelected && "border-transparent",
|
||||
count > 0 ? "cursor-pointer" : "cursor-default",
|
||||
)}
|
||||
onClick={() => count && onClick && onClick(new Date(date).toDateString())}
|
||||
>
|
||||
{day}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
) : (
|
||||
<div
|
||||
key={`${date}-${index}`}
|
||||
className={clsx(
|
||||
"shrink-0 opacity-30 w-6 h-6 rounded-xl flex justify-center items-center border border-transparent",
|
||||
getCellAdditionalStyles(count, maxCount),
|
||||
)}
|
||||
></div>
|
||||
<div key={`${date}-${index}`} className={clsx("shrink-0 w-6 h-6 opacity-0", getCellAdditionalStyles(count, maxCount))}></div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
@ -9,7 +9,6 @@ import { useMemoStore } from "@/store/v1";
|
||||
import { RowStatus } from "@/types/proto/api/v1/common";
|
||||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import showMemoEditorDialog from "./MemoEditor/MemoEditorDialog";
|
||||
|
||||
interface Props {
|
||||
memo: Memo;
|
||||
@ -55,12 +54,6 @@ const MemoActionMenu = (props: Props) => {
|
||||
props.onEdit();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: remove me later.
|
||||
showMemoEditorDialog({
|
||||
memoName: memo.name,
|
||||
cacheKey: `${memo.name}-${memo.updateTime}`,
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleMemoStatusClick = async () => {
|
||||
@ -125,7 +118,7 @@ const MemoActionMenu = (props: Props) => {
|
||||
{memo.pinned ? t("common.unpin") : t("common.pin")}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!hiddenActions?.includes("edit") && (
|
||||
{!hiddenActions?.includes("edit") && props.onEdit && (
|
||||
<MenuItem onClick={handleEditMemoClick}>
|
||||
<Icon.Edit3 className="w-4 h-auto" />
|
||||
{t("common.edit")}
|
||||
|
@ -1,103 +0,0 @@
|
||||
import { IconButton } from "@mui/joy";
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useRef } from "react";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import useDateTime from "@/hooks/useDateTime";
|
||||
import { useMemoStore, useTagStore } from "@/store/v1";
|
||||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
||||
import MemoEditor, { Props as MemoEditorProps } from ".";
|
||||
import { generateDialog } from "../Dialog";
|
||||
import Icon from "../Icon";
|
||||
|
||||
interface Props extends DialogProps, MemoEditorProps {}
|
||||
|
||||
const MemoEditorDialog: React.FC<Props> = ({
|
||||
memoName,
|
||||
parentMemoName,
|
||||
placeholder,
|
||||
cacheKey,
|
||||
relationList,
|
||||
onConfirm,
|
||||
destroy,
|
||||
}: Props) => {
|
||||
const tagStore = useTagStore();
|
||||
const memoStore = useMemoStore();
|
||||
const { setDateTime, displayDateTime, datePickerDateTime } = useDateTime(memoStore.getMemoByName(memoName || "")?.displayTime);
|
||||
const memoPatchRef = useRef<Partial<Memo>>({
|
||||
displayTime: memoStore.getMemoByName(memoName || "")?.displayTime,
|
||||
});
|
||||
const user = useCurrentUser();
|
||||
|
||||
useEffect(() => {
|
||||
tagStore.fetchTags({ user }, { skipCache: false });
|
||||
}, []);
|
||||
|
||||
const updateDisplayTime = (displayTime: string) => {
|
||||
setDateTime(displayTime);
|
||||
memoPatchRef.current.displayTime = new Date(displayTime);
|
||||
};
|
||||
|
||||
const handleCloseBtnClick = () => {
|
||||
destroy();
|
||||
};
|
||||
|
||||
const handleConfirm = (memoName: string) => {
|
||||
handleCloseBtnClick();
|
||||
if (onConfirm) {
|
||||
onConfirm(memoName);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full flex flex-row justify-between items-center">
|
||||
<div className={clsx("flex flex-row justify-start items-center", !displayDateTime && "mb-2")}>
|
||||
{displayDateTime ? (
|
||||
<div className="relative">
|
||||
<span className="cursor-pointer text-gray-500 dark:text-gray-400">{displayDateTime}</span>
|
||||
<input
|
||||
className="inset-0 absolute z-1 opacity-0"
|
||||
type="datetime-local"
|
||||
value={datePickerDateTime}
|
||||
onFocus={(e: any) => e.target.showPicker()}
|
||||
onChange={(e) => updateDisplayTime(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<img className="w-6 h-auto rounded-full shadow" src={"/full-logo.webp"} alt="" />
|
||||
<p className="ml-1 text-lg opacity-80 dark:text-gray-300">Memos</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<IconButton size="sm" onClick={handleCloseBtnClick}>
|
||||
<Icon.X className="w-5 h-auto" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className="flex flex-col justify-start items-start max-w-full w-[40rem]">
|
||||
<MemoEditor
|
||||
className="border-none !p-0 -mb-2"
|
||||
cacheKey={`memo-editor-${cacheKey || memoName}`}
|
||||
memoName={memoName}
|
||||
parentMemoName={parentMemoName}
|
||||
placeholder={placeholder}
|
||||
relationList={relationList}
|
||||
memoPatchRef={memoPatchRef}
|
||||
onConfirm={handleConfirm}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function showMemoEditorDialog(props: Partial<Props> = {}): void {
|
||||
generateDialog(
|
||||
{
|
||||
className: "memo-editor-dialog",
|
||||
dialogName: "memo-editor-dialog",
|
||||
},
|
||||
MemoEditorDialog,
|
||||
props,
|
||||
);
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { Select, Option, Button, Divider } from "@mui/joy";
|
||||
import { isEqual } from "lodash-es";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@ -6,6 +7,7 @@ import useLocalStorage from "react-use/lib/useLocalStorage";
|
||||
import { memoServiceClient } from "@/grpcweb";
|
||||
import { TAB_SPACE_WIDTH } from "@/helpers/consts";
|
||||
import { isValidUrl } from "@/helpers/utils";
|
||||
import useAsyncEffect from "@/hooks/useAsyncEffect";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { useMemoStore, useResourceStore, useUserStore, useWorkspaceSettingStore } from "@/store/v1";
|
||||
import { MemoRelation, MemoRelation_Type } from "@/types/proto/api/v1/memo_relation_service";
|
||||
@ -32,11 +34,11 @@ export interface Props {
|
||||
className?: string;
|
||||
cacheKey?: string;
|
||||
placeholder?: string;
|
||||
// The name of the memo to be edited.
|
||||
memoName?: string;
|
||||
// The name of the parent memo if the memo is a comment.
|
||||
parentMemoName?: string;
|
||||
relationList?: MemoRelation[];
|
||||
autoFocus?: boolean;
|
||||
memoPatchRef?: React.MutableRefObject<Partial<Memo>>;
|
||||
onConfirm?: (memoName: string) => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
@ -62,11 +64,12 @@ const MemoEditor = (props: Props) => {
|
||||
const [state, setState] = useState<State>({
|
||||
memoVisibility: Visibility.PRIVATE,
|
||||
resourceList: [],
|
||||
relationList: props.relationList ?? [],
|
||||
relationList: [],
|
||||
isUploadingResource: false,
|
||||
isRequesting: false,
|
||||
isComposing: false,
|
||||
});
|
||||
const [displayTime, setDisplayTime] = useState<Date | undefined>();
|
||||
const [hasContent, setHasContent] = useState<boolean>(false);
|
||||
const editorRef = useRef<EditorRefActions>(null);
|
||||
const userSetting = userStore.userSetting as UserSetting;
|
||||
@ -102,22 +105,24 @@ const MemoEditor = (props: Props) => {
|
||||
}));
|
||||
}, [userSetting.memoVisibility, workspaceMemoRelatedSetting.disallowPublicVisibility]);
|
||||
|
||||
useEffect(() => {
|
||||
if (memoName) {
|
||||
memoStore.getOrFetchMemoByName(memoName).then((memo) => {
|
||||
if (memo) {
|
||||
handleEditorFocus();
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
memoVisibility: memo.visibility,
|
||||
resourceList: memo.resources,
|
||||
relationList: memo.relations,
|
||||
}));
|
||||
if (!contentCache) {
|
||||
editorRef.current?.setContent(memo.content ?? "");
|
||||
}
|
||||
}
|
||||
});
|
||||
useAsyncEffect(async () => {
|
||||
if (!memoName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const memo = await memoStore.getOrFetchMemoByName(memoName);
|
||||
if (memo) {
|
||||
handleEditorFocus();
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
memoVisibility: memo.visibility,
|
||||
resourceList: memo.resources,
|
||||
relationList: memo.relations,
|
||||
}));
|
||||
setDisplayTime(memo.displayTime);
|
||||
if (!contentCache) {
|
||||
editorRef.current?.setContent(memo.content ?? "");
|
||||
}
|
||||
}
|
||||
}, [memoName]);
|
||||
|
||||
@ -289,18 +294,16 @@ const MemoEditor = (props: Props) => {
|
||||
const prevMemo = await memoStore.getOrFetchMemoByName(memoName);
|
||||
if (prevMemo) {
|
||||
const updateMask = ["content", "visibility"];
|
||||
if (props.memoPatchRef?.current?.displayTime) {
|
||||
const memoPatch: Partial<Memo> = {
|
||||
name: prevMemo.name,
|
||||
content,
|
||||
visibility: state.memoVisibility,
|
||||
};
|
||||
if (!isEqual(displayTime, prevMemo.displayTime)) {
|
||||
updateMask.push("display_time");
|
||||
memoPatch.displayTime = displayTime;
|
||||
}
|
||||
const memo = await memoStore.updateMemo(
|
||||
{
|
||||
name: prevMemo.name,
|
||||
content,
|
||||
visibility: state.memoVisibility,
|
||||
...props.memoPatchRef?.current,
|
||||
},
|
||||
updateMask,
|
||||
);
|
||||
const memo = await memoStore.updateMemo(memoPatch, updateMask);
|
||||
await memoServiceClient.setMemoResources({
|
||||
name: memo.name,
|
||||
resources: state.resourceList,
|
||||
@ -409,6 +412,18 @@ const MemoEditor = (props: Props) => {
|
||||
onCompositionStart={handleCompositionStart}
|
||||
onCompositionEnd={handleCompositionEnd}
|
||||
>
|
||||
{memoName && displayTime && (
|
||||
<div className="relative text-sm">
|
||||
<span className="cursor-pointer text-gray-400 dark:text-gray-500">{displayTime.toLocaleString()}</span>
|
||||
<input
|
||||
className="inset-0 absolute z-1 opacity-0"
|
||||
type="datetime-local"
|
||||
value={displayTime.toLocaleString()}
|
||||
onFocus={(e: any) => e.target.showPicker()}
|
||||
onChange={(e) => setDisplayTime(new Date(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Editor ref={editorRef} {...editorConfig} />
|
||||
<ResourceListView resourceList={state.resourceList} setResourceList={handleSetResourceList} />
|
||||
<RelationListView relationList={referenceRelations} setRelationList={handleSetRelationList} />
|
||||
|
@ -44,7 +44,7 @@ const MemoRelationListView = (props: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col justify-start items-start w-full px-2 pt-2 pb-1 bg-zinc-50 dark:bg-zinc-900 rounded-lg border border-gray-200 dark:border-zinc-700">
|
||||
<div className="relative flex flex-col justify-start items-start w-full px-2 pt-2 pb-1.5 bg-zinc-50 dark:bg-zinc-900 rounded-lg border border-gray-200 dark:border-zinc-700">
|
||||
<div className="w-full flex flex-row justify-start items-center mb-1 gap-3 opacity-60">
|
||||
{referencingMemoList.length > 0 && (
|
||||
<button
|
||||
@ -56,6 +56,7 @@ const MemoRelationListView = (props: Props) => {
|
||||
>
|
||||
<Icon.Link className="w-3 h-auto shrink-0 opacity-70" />
|
||||
<span>Referencing</span>
|
||||
<span className="opacity-80">({referencingMemoList.length})</span>
|
||||
</button>
|
||||
)}
|
||||
{referencedMemoList.length > 0 && (
|
||||
@ -68,6 +69,7 @@ const MemoRelationListView = (props: Props) => {
|
||||
>
|
||||
<Icon.Milestone className="w-3 h-auto shrink-0 opacity-70" />
|
||||
<span>Referenced by</span>
|
||||
<span className="opacity-80">({referencedMemoList.length})</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@ -81,7 +83,7 @@ const MemoRelationListView = (props: Props) => {
|
||||
to={`/m/${memo.uid}`}
|
||||
unstable_viewTransition
|
||||
>
|
||||
<Icon.Dot className="shrink-0 -ml-1 opacity-60" />
|
||||
<Icon.Dot className="shrink-0 w-4 h-auto opacity-40" />
|
||||
<span className="truncate">{memo.snippet}</span>
|
||||
</Link>
|
||||
);
|
||||
@ -98,7 +100,7 @@ const MemoRelationListView = (props: Props) => {
|
||||
to={`/m/${memo.uid}`}
|
||||
unstable_viewTransition
|
||||
>
|
||||
<Icon.Dot className="shrink-0 -ml-1 opacity-60" />
|
||||
<Icon.Dot className="shrink-0 w-4 h-auto opacity-40" />
|
||||
<span className="truncate">{memo.snippet}</span>
|
||||
</Link>
|
||||
);
|
||||
|
@ -106,77 +106,6 @@ const MemoView: React.FC<Props> = (props: Props) => {
|
||||
)}
|
||||
ref={memoContainerRef}
|
||||
>
|
||||
<div className="w-full flex flex-row justify-between items-center gap-2">
|
||||
<div className="w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center">
|
||||
{props.showCreator && creator ? (
|
||||
<div className="w-full flex flex-row justify-start items-center">
|
||||
<Link className="w-auto hover:opacity-80" to={`/u/${encodeURIComponent(creator.username)}`} unstable_viewTransition>
|
||||
<UserAvatar className="mr-2 shrink-0" avatarUrl={creator.avatarUrl} />
|
||||
</Link>
|
||||
<div className="w-full flex flex-col justify-center items-start">
|
||||
<Link
|
||||
className="w-full block leading-tight hover:opacity-80 truncate text-gray-600 dark:text-gray-400"
|
||||
to={`/u/${encodeURIComponent(creator.username)}`}
|
||||
unstable_viewTransition
|
||||
>
|
||||
{creator.nickname || creator.username}
|
||||
</Link>
|
||||
<div
|
||||
className="w-auto -mt-0.5 text-xs leading-tight text-gray-400 dark:text-gray-500 select-none"
|
||||
onClick={handleGotoMemoDetailPage}
|
||||
>
|
||||
{displayTime}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full text-sm leading-tight text-gray-400 dark:text-gray-500 select-none" onClick={handleGotoMemoDetailPage}>
|
||||
{displayTime}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!showEditor && (
|
||||
<div className="flex flex-row justify-end items-center select-none shrink-0 gap-2">
|
||||
<div className="w-auto invisible group-hover:visible flex flex-row justify-between items-center gap-2">
|
||||
{props.showVisibility && memo.visibility !== Visibility.PRIVATE && (
|
||||
<Tooltip title={t(`memo.visibility.${convertVisibilityToString(memo.visibility).toLowerCase()}` as any)} placement="top">
|
||||
<span className="flex justify-center items-center hover:opacity-70">
|
||||
<VisibilityIcon visibility={memo.visibility} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{currentUser && <ReactionSelector className="border-none w-auto h-auto" memo={memo} />}
|
||||
</div>
|
||||
{!isInMemoDetailPage && (
|
||||
<Link
|
||||
className={clsx(
|
||||
"flex flex-row justify-start items-center hover:opacity-70",
|
||||
commentAmount === 0 && "invisible group-hover:visible",
|
||||
)}
|
||||
to={`/m/${memo.uid}#comments`}
|
||||
unstable_viewTransition
|
||||
>
|
||||
<Icon.MessageCircleMore className="w-4 h-4 mx-auto text-gray-500 dark:text-gray-400" />
|
||||
{commentAmount > 0 && <span className="text-xs text-gray-500 dark:text-gray-400">{commentAmount}</span>}
|
||||
</Link>
|
||||
)}
|
||||
{props.showPinned && memo.pinned && (
|
||||
<Tooltip title={t("common.pinned")} placement="top">
|
||||
<Icon.Bookmark className="w-4 h-auto text-amber-500" />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!readonly && (
|
||||
<MemoActionMenu
|
||||
className="-ml-1"
|
||||
memo={memo}
|
||||
hiddenActions={props.showPinned ? [] : ["pin"]}
|
||||
onEdit={() => setShowEditor(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showEditor ? (
|
||||
<MemoEditor
|
||||
autoFocus
|
||||
@ -188,6 +117,77 @@ const MemoView: React.FC<Props> = (props: Props) => {
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-full flex flex-row justify-between items-center gap-2">
|
||||
<div className="w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center">
|
||||
{props.showCreator && creator ? (
|
||||
<div className="w-full flex flex-row justify-start items-center">
|
||||
<Link className="w-auto hover:opacity-80" to={`/u/${encodeURIComponent(creator.username)}`} unstable_viewTransition>
|
||||
<UserAvatar className="mr-2 shrink-0" avatarUrl={creator.avatarUrl} />
|
||||
</Link>
|
||||
<div className="w-full flex flex-col justify-center items-start">
|
||||
<Link
|
||||
className="w-full block leading-tight hover:opacity-80 truncate text-gray-600 dark:text-gray-400"
|
||||
to={`/u/${encodeURIComponent(creator.username)}`}
|
||||
unstable_viewTransition
|
||||
>
|
||||
{creator.nickname || creator.username}
|
||||
</Link>
|
||||
<div
|
||||
className="w-auto -mt-0.5 text-xs leading-tight text-gray-400 dark:text-gray-500 select-none"
|
||||
onClick={handleGotoMemoDetailPage}
|
||||
>
|
||||
{displayTime}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="w-full text-sm leading-tight text-gray-400 dark:text-gray-500 select-none"
|
||||
onClick={handleGotoMemoDetailPage}
|
||||
>
|
||||
{displayTime}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row justify-end items-center select-none shrink-0 gap-2">
|
||||
<div className="w-auto invisible group-hover:visible flex flex-row justify-between items-center gap-2">
|
||||
{props.showVisibility && memo.visibility !== Visibility.PRIVATE && (
|
||||
<Tooltip title={t(`memo.visibility.${convertVisibilityToString(memo.visibility).toLowerCase()}` as any)} placement="top">
|
||||
<span className="flex justify-center items-center hover:opacity-70">
|
||||
<VisibilityIcon visibility={memo.visibility} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{currentUser && <ReactionSelector className="border-none w-auto h-auto" memo={memo} />}
|
||||
</div>
|
||||
{!isInMemoDetailPage && (
|
||||
<Link
|
||||
className={clsx(
|
||||
"flex flex-row justify-start items-center hover:opacity-70",
|
||||
commentAmount === 0 && "invisible group-hover:visible",
|
||||
)}
|
||||
to={`/m/${memo.uid}#comments`}
|
||||
unstable_viewTransition
|
||||
>
|
||||
<Icon.MessageCircleMore className="w-4 h-4 mx-auto text-gray-500 dark:text-gray-400" />
|
||||
{commentAmount > 0 && <span className="text-xs text-gray-500 dark:text-gray-400">{commentAmount}</span>}
|
||||
</Link>
|
||||
)}
|
||||
{props.showPinned && memo.pinned && (
|
||||
<Tooltip title={t("common.pinned")} placement="top">
|
||||
<Icon.Bookmark className="w-4 h-auto text-amber-500" />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!readonly && (
|
||||
<MemoActionMenu
|
||||
className="-ml-1"
|
||||
memo={memo}
|
||||
hiddenActions={props.showPinned ? [] : ["pin"]}
|
||||
onEdit={() => setShowEditor(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<MemoContent
|
||||
key={`${memo.name}-${memo.updateTime}`}
|
||||
memoName={memo.name}
|
||||
|
@ -12,6 +12,7 @@ import { useMemoStore } from "@/store/v1";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import ActivityCalendar from "./ActivityCalendar";
|
||||
import Icon from "./Icon";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/Popover";
|
||||
|
||||
interface UserMemoStats {
|
||||
link: number;
|
||||
@ -26,15 +27,14 @@ const UserStatisticsView = () => {
|
||||
const memoStore = useMemoStore();
|
||||
const filterStore = useFilterStore();
|
||||
const [memoAmount, setMemoAmount] = useState(0);
|
||||
const [isRequesting, setIsRequesting] = useState(false);
|
||||
const [memoStats, setMemoStats] = useState<UserMemoStats>({ link: 0, taskList: 0, code: 0, incompleteTasks: 0 });
|
||||
const [activityStats, setActivityStats] = useState<Record<string, number>>({});
|
||||
const monthString = dayjs(new Date().toDateString()).format("YYYY-MM");
|
||||
const [selectedDate] = useState(new Date());
|
||||
const [monthString, setMonthString] = useState(dayjs(selectedDate.toDateString()).format("YYYY-MM"));
|
||||
const days = Math.ceil((Date.now() - currentUser.createTime!.getTime()) / 86400000);
|
||||
const filter = filterStore.state;
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
setIsRequesting(true);
|
||||
const { properties } = await memoServiceClient.listMemoProperties({
|
||||
name: `memos/-`,
|
||||
});
|
||||
@ -62,7 +62,6 @@ const UserStatisticsView = () => {
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
filter: filters.join(" && "),
|
||||
});
|
||||
|
||||
setActivityStats(
|
||||
Object.fromEntries(
|
||||
Object.entries(stats).filter(([date]) => {
|
||||
@ -70,7 +69,6 @@ const UserStatisticsView = () => {
|
||||
}),
|
||||
),
|
||||
);
|
||||
setIsRequesting(false);
|
||||
}, [memoStore.stateId]);
|
||||
|
||||
const handleRebuildMemoTags = async () => {
|
||||
@ -83,92 +81,94 @@ const UserStatisticsView = () => {
|
||||
|
||||
return (
|
||||
<div className="group w-full border mt-2 py-2 px-3 rounded-lg space-y-0.5 text-gray-500 dark:text-gray-400 bg-zinc-50 dark:bg-zinc-900 dark:border-zinc-800">
|
||||
<div className="w-full mb-1 flex flex-row justify-between items-center">
|
||||
<p className="text-sm font-medium leading-6 dark:text-gray-400">
|
||||
{new Date().toLocaleDateString(i18n.language, { month: "long", day: "numeric" })}
|
||||
</p>
|
||||
<div className="group-hover:block hidden">
|
||||
<Tooltip title={"Refresh"} placement="top">
|
||||
<Icon.RefreshCcw
|
||||
className="text-gray-400 w-4 h-auto cursor-pointer opacity-60 hover:opacity-100"
|
||||
onClick={handleRebuildMemoTags}
|
||||
/>
|
||||
</Tooltip>
|
||||
<div className="w-full mb-2 flex flex-row justify-between items-center">
|
||||
<div className="relative text-base font-medium leading-6 flex flex-row items-center dark:text-gray-400">
|
||||
<Icon.CalendarDays className="w-5 h-auto mr-1 opacity-60" strokeWidth={1.5} />
|
||||
<span>{new Date(monthString).toLocaleString(i18n.language, { year: "numeric", month: "long" })}</span>
|
||||
<input
|
||||
className="inset-0 absolute z-1 opacity-0"
|
||||
type="month"
|
||||
value={monthString}
|
||||
onFocus={(e: any) => e.target.showPicker()}
|
||||
onChange={(e) => setMonthString(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="invisible group-hover:visible flex justify-end items-center">
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Icon.MoreVertical className="w-4 h-auto shrink-0 opacity-60" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<button className="w-auto flex flex-row justify-between items-center gap-2 hover:opacity-80" onClick={handleRebuildMemoTags}>
|
||||
<Icon.RefreshCcw className="text-gray-400 w-4 h-auto cursor-pointer opacity-60" />
|
||||
<span className="text-sm shrink-0 text-gray-500 dark:text-gray-400">Refresh</span>
|
||||
</button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full pb-2">
|
||||
<ActivityCalendar month={monthString} selectedDate={new Date().toDateString()} data={activityStats} />
|
||||
<div className="w-full">
|
||||
<ActivityCalendar month={monthString} selectedDate={selectedDate.toDateString()} data={activityStats} />
|
||||
{memoAmount > 0 && (
|
||||
<p className="mt-1 w-full text-xs italic opacity-80">
|
||||
<span>{memoAmount}</span> memos in <span>{days}</span> days
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full grid grid-cols-1 gap-x-4">
|
||||
<div className="w-full flex justify-between items-center">
|
||||
<div className="w-auto flex justify-start items-center">
|
||||
<Icon.CalendarDays className="w-4 h-auto mr-1" />
|
||||
<span className="block text-base sm:text-sm">Days</span>
|
||||
<Divider className="!my-2 opacity-50" />
|
||||
<div className="w-full flex flex-row justify-start items-center gap-x-2 gap-y-1 flex-wrap">
|
||||
<div
|
||||
className={clsx(
|
||||
"w-auto border dark:border-zinc-800 pl-1 pr-1.5 rounded-md flex justify-between items-center cursor-pointer hover:shadow",
|
||||
filter.memoPropertyFilter?.hasLink ? "bg-blue-50 dark:bg-blue-900 shadow" : "",
|
||||
)}
|
||||
onClick={() => filterStore.setMemoPropertyFilter({ hasLink: !filter.memoPropertyFilter?.hasLink })}
|
||||
>
|
||||
<div className="w-auto flex justify-start items-center mr-1">
|
||||
<Icon.Link className="w-4 h-auto mr-1" />
|
||||
<span className="block text-sm">{t("memo.links")}</span>
|
||||
</div>
|
||||
<span>{days}</span>
|
||||
<span className="text-sm truncate">{memoStats.link}</span>
|
||||
</div>
|
||||
<div className="w-full flex justify-between items-center">
|
||||
<div className="w-auto flex justify-start items-center">
|
||||
<Icon.Library className="w-4 h-auto mr-1" />
|
||||
<span className="block text-base sm:text-sm">Memos</span>
|
||||
</div>
|
||||
{isRequesting ? <Icon.Loader className="animate-spin w-4 h-auto text-gray-400" /> : <span className="">{memoAmount}</span>}
|
||||
</div>
|
||||
<Divider className="!my-1 opacity-50" />
|
||||
<div className="w-full mt-1 flex flex-row justify-start items-center gap-x-2 gap-y-1 flex-wrap">
|
||||
<div
|
||||
className={clsx(
|
||||
"w-auto border dark:border-zinc-800 pl-1 pr-1.5 rounded-md flex justify-between items-center cursor-pointer hover:shadow",
|
||||
filter.memoPropertyFilter?.hasLink ? "bg-blue-50 dark:bg-blue-900 shadow" : "",
|
||||
)}
|
||||
onClick={() => filterStore.setMemoPropertyFilter({ hasLink: !filter.memoPropertyFilter?.hasLink })}
|
||||
>
|
||||
<div className="w-auto flex justify-start items-center mr-1">
|
||||
<Icon.Link className="w-4 h-auto mr-1" />
|
||||
<span className="block text-sm">{t("memo.links")}</span>
|
||||
</div>
|
||||
<span className="text-sm truncate">{memoStats.link}</span>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
"w-auto border dark:border-zinc-800 pl-1 pr-1.5 rounded-md flex justify-between items-center cursor-pointer hover:shadow",
|
||||
filter.memoPropertyFilter?.hasTaskList ? "bg-blue-50 dark:bg-blue-900 shadow" : "",
|
||||
)}
|
||||
onClick={() => filterStore.setMemoPropertyFilter({ hasTaskList: !filter.memoPropertyFilter?.hasTaskList })}
|
||||
>
|
||||
<div className="w-auto flex justify-start items-center mr-1">
|
||||
{memoStats.incompleteTasks > 0 ? (
|
||||
<Icon.ListTodo className="w-4 h-auto mr-1" />
|
||||
) : (
|
||||
<Icon.CheckCircle className="w-4 h-auto mr-1" />
|
||||
)}
|
||||
<span className="block text-sm">{t("memo.to-do")}</span>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
"w-auto border dark:border-zinc-800 pl-1 pr-1.5 rounded-md flex justify-between items-center cursor-pointer hover:shadow",
|
||||
filter.memoPropertyFilter?.hasTaskList ? "bg-blue-50 dark:bg-blue-900 shadow" : "",
|
||||
)}
|
||||
onClick={() => filterStore.setMemoPropertyFilter({ hasTaskList: !filter.memoPropertyFilter?.hasTaskList })}
|
||||
>
|
||||
<div className="w-auto flex justify-start items-center mr-1">
|
||||
{memoStats.incompleteTasks > 0 ? (
|
||||
<Tooltip title={"Done / Total"} placement="top" arrow>
|
||||
<div className="text-sm flex flex-row items-start justify-center">
|
||||
<span className="truncate">{memoStats.taskList - memoStats.incompleteTasks}</span>
|
||||
<span className="font-mono opacity-50">/</span>
|
||||
<span className="truncate">{memoStats.taskList}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Icon.ListTodo className="w-4 h-auto mr-1" />
|
||||
) : (
|
||||
<span className="text-sm truncate">{memoStats.taskList}</span>
|
||||
<Icon.CheckCircle className="w-4 h-auto mr-1" />
|
||||
)}
|
||||
<span className="block text-sm">{t("memo.to-do")}</span>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
"w-auto border dark:border-zinc-800 pl-1 pr-1.5 rounded-md flex justify-between items-center cursor-pointer hover:shadow",
|
||||
filter.memoPropertyFilter?.hasCode ? "bg-blue-50 dark:bg-blue-900 shadow" : "",
|
||||
)}
|
||||
onClick={() => filterStore.setMemoPropertyFilter({ hasCode: !filter.memoPropertyFilter?.hasCode })}
|
||||
>
|
||||
<div className="w-auto flex justify-start items-center mr-1">
|
||||
<Icon.Code2 className="w-4 h-auto mr-1" />
|
||||
<span className="block text-sm">{t("memo.code")}</span>
|
||||
</div>
|
||||
<span className="text-sm truncate">{memoStats.code}</span>
|
||||
{memoStats.incompleteTasks > 0 ? (
|
||||
<Tooltip title={"Done / Total"} placement="top" arrow>
|
||||
<div className="text-sm flex flex-row items-start justify-center">
|
||||
<span className="truncate">{memoStats.taskList - memoStats.incompleteTasks}</span>
|
||||
<span className="font-mono opacity-50">/</span>
|
||||
<span className="truncate">{memoStats.taskList}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className="text-sm truncate">{memoStats.taskList}</span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
"w-auto border dark:border-zinc-800 pl-1 pr-1.5 rounded-md flex justify-between items-center cursor-pointer hover:shadow",
|
||||
filter.memoPropertyFilter?.hasCode ? "bg-blue-50 dark:bg-blue-900 shadow" : "",
|
||||
)}
|
||||
onClick={() => filterStore.setMemoPropertyFilter({ hasCode: !filter.memoPropertyFilter?.hasCode })}
|
||||
>
|
||||
<div className="w-auto flex justify-start items-center mr-1">
|
||||
<Icon.Code2 className="w-4 h-auto mr-1" />
|
||||
<span className="block text-sm">{t("memo.code")}</span>
|
||||
</div>
|
||||
<span className="text-sm truncate">{memoStats.code}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,4 +4,3 @@ export * from "./useNavigateTo";
|
||||
export * from "./useAsyncEffect";
|
||||
export * from "./useFilterWithUrlParams";
|
||||
export * from "./useResponsiveWidth";
|
||||
export * from "./useDateTime";
|
||||
|
@ -1,13 +0,0 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const useDateTime = (initalState?: Date) => {
|
||||
const [dateTime, setDateTimeInternal] = useState<Date | undefined>(initalState && new Date(initalState));
|
||||
|
||||
return {
|
||||
setDateTime: (dateTimeString: string) => setDateTimeInternal(new Date(dateTimeString)),
|
||||
displayDateTime: dateTime && dateTime.toLocaleString(),
|
||||
datePickerDateTime: dateTime && new Date(dateTime.getTime() - dateTime.getTimezoneOffset() * 60000).toISOString().split(".")[0],
|
||||
};
|
||||
};
|
||||
|
||||
export default useDateTime;
|
Reference in New Issue
Block a user