feat: update memo editor

This commit is contained in:
Steven
2024-07-22 09:57:40 +08:00
parent d2727e6825
commit c313596144
9 changed files with 227 additions and 327 deletions

View File

@ -18,11 +18,11 @@ const getCellAdditionalStyles = (count: number, maxCount: number) => {
const ratio = count / maxCount; const ratio = count / maxCount;
if (ratio > 0.7) { 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) { } 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 { } 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 tooltipText = count ? t("memo.count-memos-in-date", { count: count, date: date }) : date;
const isSelected = new Date(props.selectedDate).toDateString() === new Date(date).toDateString(); const isSelected = new Date(props.selectedDate).toDateString() === new Date(date).toDateString();
return day ? ( 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 <div
key={`${date}-${index}`}
className={clsx( className={clsx(
"w-6 h-6 text-xs rounded-xl flex justify-center items-center border", "w-6 h-6 text-xs rounded-xl flex justify-center items-center border cursor-default",
getCellAdditionalStyles(count, maxCount), "bg-gray-100 text-gray-400 dark:bg-zinc-800 dark:text-gray-500",
isToday && "border-gray-600 dark:border-zinc-300", isToday && "border-zinc-400 dark:border-zinc-500",
isSelected && "font-bold border-gray-600 dark:border-zinc-300",
!isToday && !isSelected && "border-transparent", !isToday && !isSelected && "border-transparent",
count > 0 ? "cursor-pointer" : "cursor-default",
)} )}
onClick={() => count && onClick && onClick(new Date(date).toDateString())}
> >
{day} {day}
</div> </div>
</Tooltip> )
) : ( ) : (
<div <div key={`${date}-${index}`} className={clsx("shrink-0 w-6 h-6 opacity-0", getCellAdditionalStyles(count, maxCount))}></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> </div>

View File

@ -9,7 +9,6 @@ import { useMemoStore } from "@/store/v1";
import { RowStatus } from "@/types/proto/api/v1/common"; import { RowStatus } from "@/types/proto/api/v1/common";
import { Memo } from "@/types/proto/api/v1/memo_service"; import { Memo } from "@/types/proto/api/v1/memo_service";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import showMemoEditorDialog from "./MemoEditor/MemoEditorDialog";
interface Props { interface Props {
memo: Memo; memo: Memo;
@ -55,12 +54,6 @@ const MemoActionMenu = (props: Props) => {
props.onEdit(); props.onEdit();
return; return;
} }
// TODO: remove me later.
showMemoEditorDialog({
memoName: memo.name,
cacheKey: `${memo.name}-${memo.updateTime}`,
});
}; };
const handleToggleMemoStatusClick = async () => { const handleToggleMemoStatusClick = async () => {
@ -125,7 +118,7 @@ const MemoActionMenu = (props: Props) => {
{memo.pinned ? t("common.unpin") : t("common.pin")} {memo.pinned ? t("common.unpin") : t("common.pin")}
</MenuItem> </MenuItem>
)} )}
{!hiddenActions?.includes("edit") && ( {!hiddenActions?.includes("edit") && props.onEdit && (
<MenuItem onClick={handleEditMemoClick}> <MenuItem onClick={handleEditMemoClick}>
<Icon.Edit3 className="w-4 h-auto" /> <Icon.Edit3 className="w-4 h-auto" />
{t("common.edit")} {t("common.edit")}

View File

@ -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,
);
}

View File

@ -1,4 +1,5 @@
import { Select, Option, Button, Divider } from "@mui/joy"; import { Select, Option, Button, Divider } from "@mui/joy";
import { isEqual } from "lodash-es";
import React, { useEffect, useMemo, useRef, useState } from "react"; import React, { useEffect, useMemo, 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";
@ -6,6 +7,7 @@ import useLocalStorage from "react-use/lib/useLocalStorage";
import { memoServiceClient } from "@/grpcweb"; import { memoServiceClient } from "@/grpcweb";
import { TAB_SPACE_WIDTH } from "@/helpers/consts"; import { TAB_SPACE_WIDTH } from "@/helpers/consts";
import { isValidUrl } from "@/helpers/utils"; import { isValidUrl } from "@/helpers/utils";
import useAsyncEffect from "@/hooks/useAsyncEffect";
import useCurrentUser from "@/hooks/useCurrentUser"; import useCurrentUser from "@/hooks/useCurrentUser";
import { useMemoStore, useResourceStore, useUserStore, useWorkspaceSettingStore } from "@/store/v1"; import { useMemoStore, useResourceStore, useUserStore, useWorkspaceSettingStore } from "@/store/v1";
import { MemoRelation, MemoRelation_Type } from "@/types/proto/api/v1/memo_relation_service"; import { MemoRelation, MemoRelation_Type } from "@/types/proto/api/v1/memo_relation_service";
@ -32,11 +34,11 @@ export interface Props {
className?: string; className?: string;
cacheKey?: string; cacheKey?: string;
placeholder?: string; placeholder?: string;
// The name of the memo to be edited.
memoName?: string; memoName?: string;
// The name of the parent memo if the memo is a comment.
parentMemoName?: string; parentMemoName?: string;
relationList?: MemoRelation[];
autoFocus?: boolean; autoFocus?: boolean;
memoPatchRef?: React.MutableRefObject<Partial<Memo>>;
onConfirm?: (memoName: string) => void; onConfirm?: (memoName: string) => void;
onCancel?: () => void; onCancel?: () => void;
} }
@ -62,11 +64,12 @@ const MemoEditor = (props: Props) => {
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
memoVisibility: Visibility.PRIVATE, memoVisibility: Visibility.PRIVATE,
resourceList: [], resourceList: [],
relationList: props.relationList ?? [], relationList: [],
isUploadingResource: false, isUploadingResource: false,
isRequesting: false, isRequesting: false,
isComposing: false, isComposing: false,
}); });
const [displayTime, setDisplayTime] = useState<Date | undefined>();
const [hasContent, setHasContent] = useState<boolean>(false); const [hasContent, setHasContent] = useState<boolean>(false);
const editorRef = useRef<EditorRefActions>(null); const editorRef = useRef<EditorRefActions>(null);
const userSetting = userStore.userSetting as UserSetting; const userSetting = userStore.userSetting as UserSetting;
@ -102,22 +105,24 @@ const MemoEditor = (props: Props) => {
})); }));
}, [userSetting.memoVisibility, workspaceMemoRelatedSetting.disallowPublicVisibility]); }, [userSetting.memoVisibility, workspaceMemoRelatedSetting.disallowPublicVisibility]);
useEffect(() => { useAsyncEffect(async () => {
if (memoName) { if (!memoName) {
memoStore.getOrFetchMemoByName(memoName).then((memo) => { return;
if (memo) { }
handleEditorFocus();
setState((prevState) => ({ const memo = await memoStore.getOrFetchMemoByName(memoName);
...prevState, if (memo) {
memoVisibility: memo.visibility, handleEditorFocus();
resourceList: memo.resources, setState((prevState) => ({
relationList: memo.relations, ...prevState,
})); memoVisibility: memo.visibility,
if (!contentCache) { resourceList: memo.resources,
editorRef.current?.setContent(memo.content ?? ""); relationList: memo.relations,
} }));
} setDisplayTime(memo.displayTime);
}); if (!contentCache) {
editorRef.current?.setContent(memo.content ?? "");
}
} }
}, [memoName]); }, [memoName]);
@ -289,18 +294,16 @@ const MemoEditor = (props: Props) => {
const prevMemo = await memoStore.getOrFetchMemoByName(memoName); const prevMemo = await memoStore.getOrFetchMemoByName(memoName);
if (prevMemo) { if (prevMemo) {
const updateMask = ["content", "visibility"]; 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"); updateMask.push("display_time");
memoPatch.displayTime = displayTime;
} }
const memo = await memoStore.updateMemo( const memo = await memoStore.updateMemo(memoPatch, updateMask);
{
name: prevMemo.name,
content,
visibility: state.memoVisibility,
...props.memoPatchRef?.current,
},
updateMask,
);
await memoServiceClient.setMemoResources({ await memoServiceClient.setMemoResources({
name: memo.name, name: memo.name,
resources: state.resourceList, resources: state.resourceList,
@ -409,6 +412,18 @@ const MemoEditor = (props: Props) => {
onCompositionStart={handleCompositionStart} onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd} 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} /> <Editor ref={editorRef} {...editorConfig} />
<ResourceListView resourceList={state.resourceList} setResourceList={handleSetResourceList} /> <ResourceListView resourceList={state.resourceList} setResourceList={handleSetResourceList} />
<RelationListView relationList={referenceRelations} setRelationList={handleSetRelationList} /> <RelationListView relationList={referenceRelations} setRelationList={handleSetRelationList} />

View File

@ -44,7 +44,7 @@ const MemoRelationListView = (props: Props) => {
} }
return ( 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"> <div className="w-full flex flex-row justify-start items-center mb-1 gap-3 opacity-60">
{referencingMemoList.length > 0 && ( {referencingMemoList.length > 0 && (
<button <button
@ -56,6 +56,7 @@ const MemoRelationListView = (props: Props) => {
> >
<Icon.Link className="w-3 h-auto shrink-0 opacity-70" /> <Icon.Link className="w-3 h-auto shrink-0 opacity-70" />
<span>Referencing</span> <span>Referencing</span>
<span className="opacity-80">({referencingMemoList.length})</span>
</button> </button>
)} )}
{referencedMemoList.length > 0 && ( {referencedMemoList.length > 0 && (
@ -68,6 +69,7 @@ const MemoRelationListView = (props: Props) => {
> >
<Icon.Milestone className="w-3 h-auto shrink-0 opacity-70" /> <Icon.Milestone className="w-3 h-auto shrink-0 opacity-70" />
<span>Referenced by</span> <span>Referenced by</span>
<span className="opacity-80">({referencedMemoList.length})</span>
</button> </button>
)} )}
</div> </div>
@ -81,7 +83,7 @@ const MemoRelationListView = (props: Props) => {
to={`/m/${memo.uid}`} to={`/m/${memo.uid}`}
unstable_viewTransition 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> <span className="truncate">{memo.snippet}</span>
</Link> </Link>
); );
@ -98,7 +100,7 @@ const MemoRelationListView = (props: Props) => {
to={`/m/${memo.uid}`} to={`/m/${memo.uid}`}
unstable_viewTransition 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> <span className="truncate">{memo.snippet}</span>
</Link> </Link>
); );

View File

@ -106,77 +106,6 @@ const MemoView: React.FC<Props> = (props: Props) => {
)} )}
ref={memoContainerRef} 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 ? ( {showEditor ? (
<MemoEditor <MemoEditor
autoFocus 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 <MemoContent
key={`${memo.name}-${memo.updateTime}`} key={`${memo.name}-${memo.updateTime}`}
memoName={memo.name} memoName={memo.name}

View File

@ -12,6 +12,7 @@ import { useMemoStore } from "@/store/v1";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import ActivityCalendar from "./ActivityCalendar"; import ActivityCalendar from "./ActivityCalendar";
import Icon from "./Icon"; import Icon from "./Icon";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/Popover";
interface UserMemoStats { interface UserMemoStats {
link: number; link: number;
@ -26,15 +27,14 @@ const UserStatisticsView = () => {
const memoStore = useMemoStore(); const memoStore = useMemoStore();
const filterStore = useFilterStore(); const filterStore = useFilterStore();
const [memoAmount, setMemoAmount] = useState(0); const [memoAmount, setMemoAmount] = useState(0);
const [isRequesting, setIsRequesting] = useState(false);
const [memoStats, setMemoStats] = useState<UserMemoStats>({ link: 0, taskList: 0, code: 0, incompleteTasks: 0 }); const [memoStats, setMemoStats] = useState<UserMemoStats>({ link: 0, taskList: 0, code: 0, incompleteTasks: 0 });
const [activityStats, setActivityStats] = useState<Record<string, number>>({}); 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 days = Math.ceil((Date.now() - currentUser.createTime!.getTime()) / 86400000);
const filter = filterStore.state; const filter = filterStore.state;
useAsyncEffect(async () => { useAsyncEffect(async () => {
setIsRequesting(true);
const { properties } = await memoServiceClient.listMemoProperties({ const { properties } = await memoServiceClient.listMemoProperties({
name: `memos/-`, name: `memos/-`,
}); });
@ -62,7 +62,6 @@ const UserStatisticsView = () => {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
filter: filters.join(" && "), filter: filters.join(" && "),
}); });
setActivityStats( setActivityStats(
Object.fromEntries( Object.fromEntries(
Object.entries(stats).filter(([date]) => { Object.entries(stats).filter(([date]) => {
@ -70,7 +69,6 @@ const UserStatisticsView = () => {
}), }),
), ),
); );
setIsRequesting(false);
}, [memoStore.stateId]); }, [memoStore.stateId]);
const handleRebuildMemoTags = async () => { const handleRebuildMemoTags = async () => {
@ -83,92 +81,94 @@ const UserStatisticsView = () => {
return ( 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="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"> <div className="w-full mb-2 flex flex-row justify-between items-center">
<p className="text-sm font-medium leading-6 dark:text-gray-400"> <div className="relative text-base font-medium leading-6 flex flex-row items-center dark:text-gray-400">
{new Date().toLocaleDateString(i18n.language, { month: "long", day: "numeric" })} <Icon.CalendarDays className="w-5 h-auto mr-1 opacity-60" strokeWidth={1.5} />
</p> <span>{new Date(monthString).toLocaleString(i18n.language, { year: "numeric", month: "long" })}</span>
<div className="group-hover:block hidden"> <input
<Tooltip title={"Refresh"} placement="top"> className="inset-0 absolute z-1 opacity-0"
<Icon.RefreshCcw type="month"
className="text-gray-400 w-4 h-auto cursor-pointer opacity-60 hover:opacity-100" value={monthString}
onClick={handleRebuildMemoTags} onFocus={(e: any) => e.target.showPicker()}
/> onChange={(e) => setMonthString(e.target.value)}
</Tooltip> />
</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> </div>
<div className="w-full pb-2"> <div className="w-full">
<ActivityCalendar month={monthString} selectedDate={new Date().toDateString()} data={activityStats} /> <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>
<div className="w-full grid grid-cols-1 gap-x-4"> <Divider className="!my-2 opacity-50" />
<div className="w-full flex justify-between items-center"> <div className="w-full flex flex-row justify-start items-center gap-x-2 gap-y-1 flex-wrap">
<div className="w-auto flex justify-start items-center"> <div
<Icon.CalendarDays className="w-4 h-auto mr-1" /> className={clsx(
<span className="block text-base sm:text-sm">Days</span> "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> </div>
<span>{days}</span> <span className="text-sm truncate">{memoStats.link}</span>
</div> </div>
<div className="w-full flex justify-between items-center"> <div
<div className="w-auto flex justify-start items-center"> className={clsx(
<Icon.Library className="w-4 h-auto mr-1" /> "w-auto border dark:border-zinc-800 pl-1 pr-1.5 rounded-md flex justify-between items-center cursor-pointer hover:shadow",
<span className="block text-base sm:text-sm">Memos</span> filter.memoPropertyFilter?.hasTaskList ? "bg-blue-50 dark:bg-blue-900 shadow" : "",
</div> )}
{isRequesting ? <Icon.Loader className="animate-spin w-4 h-auto text-gray-400" /> : <span className="">{memoAmount}</span>} onClick={() => filterStore.setMemoPropertyFilter({ hasTaskList: !filter.memoPropertyFilter?.hasTaskList })}
</div> >
<Divider className="!my-1 opacity-50" /> <div className="w-auto flex justify-start items-center mr-1">
<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>
{memoStats.incompleteTasks > 0 ? ( {memoStats.incompleteTasks > 0 ? (
<Tooltip title={"Done / Total"} placement="top" arrow> <Icon.ListTodo className="w-4 h-auto mr-1" />
<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> <Icon.CheckCircle className="w-4 h-auto mr-1" />
)} )}
<span className="block text-sm">{t("memo.to-do")}</span>
</div> </div>
<div {memoStats.incompleteTasks > 0 ? (
className={clsx( <Tooltip title={"Done / Total"} placement="top" arrow>
"w-auto border dark:border-zinc-800 pl-1 pr-1.5 rounded-md flex justify-between items-center cursor-pointer hover:shadow", <div className="text-sm flex flex-row items-start justify-center">
filter.memoPropertyFilter?.hasCode ? "bg-blue-50 dark:bg-blue-900 shadow" : "", <span className="truncate">{memoStats.taskList - memoStats.incompleteTasks}</span>
)} <span className="font-mono opacity-50">/</span>
onClick={() => filterStore.setMemoPropertyFilter({ hasCode: !filter.memoPropertyFilter?.hasCode })} <span className="truncate">{memoStats.taskList}</span>
> </div>
<div className="w-auto flex justify-start items-center mr-1"> </Tooltip>
<Icon.Code2 className="w-4 h-auto mr-1" /> ) : (
<span className="block text-sm">{t("memo.code")}</span> <span className="text-sm truncate">{memoStats.taskList}</span>
</div> )}
<span className="text-sm truncate">{memoStats.code}</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> </div>
<span className="text-sm truncate">{memoStats.code}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -4,4 +4,3 @@ export * from "./useNavigateTo";
export * from "./useAsyncEffect"; export * from "./useAsyncEffect";
export * from "./useFilterWithUrlParams"; export * from "./useFilterWithUrlParams";
export * from "./useResponsiveWidth"; export * from "./useResponsiveWidth";
export * from "./useDateTime";

View File

@ -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;