diff --git a/web/src/components/ActivityCalendar.tsx b/web/src/components/ActivityCalendar.tsx new file mode 100644 index 00000000..e2191718 --- /dev/null +++ b/web/src/components/ActivityCalendar.tsx @@ -0,0 +1,66 @@ +import { Tooltip } from "@mui/joy"; +import classNames from "classnames"; + +interface Props { + // Format: 2021-1 + month: string; + data: Record; +} + +const getBgColor = (count: number, maxCount: number) => { + if (count === 0) { + return "bg-gray-100 dark:bg-gray-700"; + } + + const ratio = count / maxCount; + if (ratio > 0.7) { + return "bg-blue-600"; + } else if (ratio > 0.5) { + return "bg-blue-400"; + } else if (ratio > 0.3) { + return "bg-blue-300"; + } else { + return "bg-blue-200"; + } +}; + +const ActivityCalendar = (props: Props) => { + const { month: monthStr, data } = props; + const [year, month] = monthStr.split("-"); + const dayInMonth = new Date(Number(year), Number(month), 0).getDate(); + const firstDay = new Date(Number(year), Number(month) - 1, 1).getDay(); + const lastDay = new Date(Number(year), Number(month) - 1, dayInMonth).getDay(); + const maxCount = Math.max(...Object.values(data)); + const days = []; + + for (let i = 0; i < firstDay; i++) { + days.push(0); + } + for (let i = 1; i <= dayInMonth; i++) { + days.push(i); + } + for (let i = 0; i < 6 - lastDay; i++) { + days.push(0); + } + + return ( +
+ {days.map((day, index) => { + const date = `${year}-${month}-${day}`; + const count = data[date] || 0; + return day ? ( + +
+
+ ) : ( +
+ ); + })} +
+ ); +}; + +export default ActivityCalendar; diff --git a/web/src/components/MemoView.tsx b/web/src/components/MemoView.tsx index ec5255b1..9439a47e 100644 --- a/web/src/components/MemoView.tsx +++ b/web/src/components/MemoView.tsx @@ -1,4 +1,5 @@ import { Divider, Tooltip } from "@mui/joy"; +import classNames from "classnames"; import copy from "copy-to-clipboard"; import { memo, useEffect, useRef, useState } from "react"; import { toast } from "react-hot-toast"; @@ -30,13 +31,13 @@ import "@/less/memo.less"; interface Props { memo: Memo; showCreator?: boolean; - showParent?: boolean; showVisibility?: boolean; showPinnedStyle?: boolean; + className?: string; } const MemoView: React.FC = (props: Props) => { - const { memo } = props; + const { memo, className } = props; const t = useTranslate(); const navigateTo = useNavigateTo(); const { i18n } = useTranslation(); @@ -165,7 +166,7 @@ const MemoView: React.FC = (props: Props) => { return (
diff --git a/web/src/components/TimelineMemo.tsx b/web/src/components/TimelineMemo.tsx deleted file mode 100644 index 33074d16..00000000 --- a/web/src/components/TimelineMemo.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import MemoContent from "@/components/MemoContent"; -import MemoResourceListView from "@/components/MemoResourceListView"; -import { getTimeString } from "@/helpers/datetime"; -import { MemoRelation_Type } from "@/types/proto/api/v2/memo_relation_service"; -import { Memo } from "@/types/proto/api/v2/memo_service"; -import MemoRelationListView from "./MemoRelationListView"; - -interface Props { - memo: Memo; -} - -const TimelineMemo = (props: Props) => { - const { memo } = props; - const relations = memo.relations.filter((relation) => relation.type === MemoRelation_Type.REFERENCE); - - return ( -
-
- {getTimeString(memo.displayTime)} -
- - - -
- ); -}; - -export default TimelineMemo; diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index 4b37bc36..e71873d7 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -56,7 +56,7 @@ const Explore = () => {
{sortedMemos.map((memo) => ( - + ))} {isRequesting ? ( diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 6397dae7..cb4590c3 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -73,7 +73,7 @@ const Home = () => {
{sortedMemos.map((memo) => ( - + ))} {isRequesting ? (
diff --git a/web/src/pages/Inboxes.tsx b/web/src/pages/Inboxes.tsx index d3ec68a4..63984ada 100644 --- a/web/src/pages/Inboxes.tsx +++ b/web/src/pages/Inboxes.tsx @@ -25,8 +25,9 @@ const Inboxes = () => {
-

- {t("common.inbox")} +

+ + {t("common.inbox")}

diff --git a/web/src/pages/Resources.tsx b/web/src/pages/Resources.tsx index aaa0f8ff..415e1390 100644 --- a/web/src/pages/Resources.tsx +++ b/web/src/pages/Resources.tsx @@ -71,8 +71,9 @@ const Resources = () => {
-

- {t("common.resources")} +

+ + {t("common.resources")}

; + memos: Memo[]; +} + +const groupByMonth = (dateCountMap: Record, memos: Memo[]): GroupedByMonthItem[] => { + const groupedByMonth: GroupedByMonthItem[] = []; + + Object.entries(dateCountMap).forEach(([date, count]) => { + const month = date.split("-").slice(0, 2).join("-"); + const existingMonth = groupedByMonth.find((group) => group.month === month); + if (existingMonth) { + existingMonth.data[date] = count; + } else { + const monthMemos = memos.filter((memo) => getNormalizedTimeString(memo.displayTime).startsWith(month)); + groupedByMonth.push({ month, data: { [date]: count }, memos: monthMemos }); + } + }); + + return groupedByMonth.filter((group) => group.memos.length > 0).sort((a, b) => getTimeStampByDate(b.month) - getTimeStampByDate(a.month)); +}; + const Timeline = () => { const t = useTranslate(); - const [searchParams, setSearchParams] = useSearchParams(); + const { md } = useResponsiveWidth(); const user = useCurrentUser(); const memoStore = useMemoStore(); const memoList = useMemoList(); - const currentDateStamp = getDateStampByDate(getNormalizedDateString()) as number; - const [selectedDateStamp, setSelectedDateStamp] = useState( - (searchParams.get("timestamp") ? Number(searchParams.get("timestamp")) : currentDateStamp) as number - ); + const [activityStats, setActivityStats] = useState>({}); const [isRequesting, setIsRequesting] = useState(true); - const [showDatePicker, toggleShowDatePicker] = useToggle(false); - const sortedMemos = memoList.value.sort((a, b) => getTimeStampByDate(a.createTime) - getTimeStampByDate(b.createTime)); + const [isComplete, setIsComplete] = useState(false); + const sortedMemos = memoList.value.sort((a, b) => getTimeStampByDate(b.displayTime) - getTimeStampByDate(a.displayTime)); + const groupedByMonth = groupByMonth(activityStats, sortedMemos); useEffect(() => { - setSearchParams(); + memoList.reset(); + fetchMemos(); }, []); useEffect(() => { - memoList.reset(); - fetchMemos(); - }, [selectedDateStamp]); + (async () => { + const { stats } = await memoServiceClient.getUserMemosStats({ + name: user.name, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }); + setActivityStats(stats); + })(); + }, [sortedMemos.length]); const fetchMemos = async () => { - const filters = [ - `creator == "${user.name}"`, - `row_status == "NORMAL"`, - `created_ts_after == ${selectedDateStamp / 1000}`, - `created_ts_before == ${(selectedDateStamp + DAILY_TIMESTAMP) / 1000}`, - ]; + const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`]; setIsRequesting(true); - await memoStore.fetchMemos({ + const data = await memoStore.fetchMemos({ filter: filters.join(" && "), + limit: DEFAULT_MEMO_LIMIT, offset: memoList.size(), }); setIsRequesting(false); + setIsComplete(data.length < DEFAULT_MEMO_LIMIT); }; - const handleDataPickerChange = (datestamp: number): void => { - setSelectedDateStamp(datestamp); - toggleShowDatePicker(false); + const handleNewMemo = () => { + showMemoEditorDialog({}); }; return ( @@ -62,63 +89,75 @@ const Timeline = () => {
-
-

toggleShowDatePicker()} - > - - {new Date(selectedDateStamp).toLocaleDateString()} -

- {selectedDateStamp !== currentDateStamp && ( - - )} - toggleShowDatePicker(false)} - /> +
+
+

+ + {t("timeline.title")} +

+
+
+ handleNewMemo()}> + + +
-
-
- {sortedMemos.map((memo, index) => ( -
- -
- {index !== sortedMemos.length - 1 && ( -
- )} -
- -
+
+ {groupedByMonth.map((group) => ( +
+
+
+ + {new Date(group.month).toLocaleString(i18n.language, { month: "short" })} + + {new Date(group.month).getFullYear()}
+
- ))} - {!isRequesting && sortedMemos.length === 0 && ( -
- -

{t("message.no-data")}

+ +
+ {group.memos.map((memo, index) => ( +
+ + {group.memos.length > 1 && ( +
+ {index !== group.memos.length - 1 && ( +
+ )} +
+ +
+
+ )} +
+ ))}
- )} - {selectedDateStamp === currentDateStamp && ( -
- +
+ ))} + {isRequesting ? ( +
+

{t("memo.fetching-data")}

+
+ ) : isComplete ? ( + sortedMemos.length === 0 && ( +
+ +

{t("message.no-data")}

- )} -
+ ) + ) : ( +
+ +
+ )}
diff --git a/web/src/pages/UserProfile.tsx b/web/src/pages/UserProfile.tsx index 675d8e91..1495248d 100644 --- a/web/src/pages/UserProfile.tsx +++ b/web/src/pages/UserProfile.tsx @@ -92,7 +92,7 @@ const UserProfile = () => {
{sortedMemos.map((memo) => ( - + ))} {isRequesting ? (