From f508a82d1a0ce37a84c50800f7b47eec72f68c95 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 16 Apr 2024 11:44:45 +0800 Subject: [PATCH] feat(Timeline): Enhance date filtering and navigation functionalities - Implemented date filtering functionality in the Timeline component. - Added buttons to navigate to previous and next months and days. - Added localization for toast messages in both English and Chinese. - Modified UI to show navigation buttons only when a specific day is selected. - Enhanced UI to visually differentiate the selected date. --- web/src/components/ActivityCalendar.tsx | 5 +- web/src/locales/en.json | 3 +- web/src/locales/zh-Hans.json | 3 +- web/src/pages/Timeline.tsx | 121 +++++++++++++++++++++++- 4 files changed, 126 insertions(+), 6 deletions(-) diff --git a/web/src/components/ActivityCalendar.tsx b/web/src/components/ActivityCalendar.tsx index 3edd96ae8..429996bfd 100644 --- a/web/src/components/ActivityCalendar.tsx +++ b/web/src/components/ActivityCalendar.tsx @@ -8,6 +8,7 @@ interface Props { month: string; data: Record; onClick?: (date: string) => void; + selectedDate?: string; } const getCellAdditionalStyles = (count: number, maxCount: number) => { @@ -26,8 +27,8 @@ const getCellAdditionalStyles = (count: number, maxCount: number) => { }; const ActivityCalendar = (props: Props) => { + const { month: monthStr, data, onClick, selectedDate } = props; const t = useTranslate(); - const { month: monthStr, data, onClick } = props; const year = new Date(monthStr).getUTCFullYear(); const month = new Date(monthStr).getUTCMonth() + 1; const dayInMonth = new Date(year, month, 0).getDate(); @@ -55,6 +56,7 @@ const ActivityCalendar = (props: Props) => { const count = data[date] || 0; const isToday = new Date().toDateString() === new Date(date).toDateString(); const tooltipText = count ? t("memo.count-memos-in-date", { count: count, date: date }) : date; + const isSelected = date === selectedDate; return day ? (
{ "w-4 h-4 text-[9px] rounded-md flex justify-center items-center border border-transparent", getCellAdditionalStyles(count, maxCount), isToday && "border-gray-600 dark:!border-gray-500", + isSelected && "bg-green-500 text-white", )} onClick={() => count && onClick && onClick(date)} > diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 276204cfe..cc7b9d632 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -354,7 +354,8 @@ "succeed-update-customized-profile": "Profile successfully customized.", "succeed-update-additional-script": "Additional script updated successfully.", "update-succeed": "Update succeeded", - "maximum-upload-size-is": "Maximum allowed upload size is {{size}} MiB" + "maximum-upload-size-is": "Maximum allowed upload size is {{size}} MiB", + "no-more-memos": "No more memmos" }, "inbox": { "memo-comment": "{{user}} has a comment on your {{memo}}.", diff --git a/web/src/locales/zh-Hans.json b/web/src/locales/zh-Hans.json index 4dfdf6bd1..2dc1b49e8 100644 --- a/web/src/locales/zh-Hans.json +++ b/web/src/locales/zh-Hans.json @@ -173,7 +173,8 @@ "succeed-update-customized-profile": "更新自定义配置文件成功。", "succeed-vacuum-database": "清理数据库成功。", "update-succeed": "更新成功", - "user-not-found": "未找到该用户" + "user-not-found": "未找到该用户", + "no-more-memos": "没有更多Memos记录" }, "reference": { "add-references": "添加引用", diff --git a/web/src/pages/Timeline.tsx b/web/src/pages/Timeline.tsx index 956016379..6e29f8baa 100644 --- a/web/src/pages/Timeline.tsx +++ b/web/src/pages/Timeline.tsx @@ -19,6 +19,7 @@ import i18n from "@/i18n"; import { useMemoList, useMemoStore } from "@/store/v1"; import { Memo } from "@/types/proto/api/v2/memo_service"; import { useTranslate } from "@/utils/i18n"; +import toast from "react-hot-toast"; interface GroupedByMonthItem { // Format: 2021-1 @@ -58,6 +59,87 @@ const Timeline = () => { const sortedMemos = memoList.value.sort((a, b) => getTimeStampByDate(b.displayTime) - getTimeStampByDate(a.displayTime)); const groupedByMonth = groupByMonth(activityStats, sortedMemos); + const handleDateClick = (date: string) => { + if (date === selectedDay) { + setSelectedDay(undefined); + } else { + setSelectedDay(date); + } + }; + const handlePrevDay = () => { + if (!selectedDay) return; + + const currentDate = new Date(selectedDay); + currentDate.setUTCDate(currentDate.getUTCDate() - 1); + + let found = false; + while (currentDate >= new Date(Object.keys(activityStats).sort()[0])) { + const checkDateStr = currentDate.toISOString().substring(0, 10); + if (activityStats[checkDateStr] && activityStats[checkDateStr] > 0) { + setSelectedDay(checkDateStr); + found = true; + break; + } + currentDate.setUTCDate(currentDate.getUTCDate() - 1); + } + + if (!found) { + toast.error(t("message.no-more-memos")); + } + }; + const handleNextDay = () => { + if (!selectedDay) return; + + const currentDate = new Date(selectedDay); + currentDate.setUTCDate(currentDate.getUTCDate() + 1); + + let found = false; + while (currentDate <= new Date(Object.keys(activityStats).sort().reverse()[0])) { + const checkDateStr = currentDate.toISOString().substring(0, 10); + if (activityStats[checkDateStr] && activityStats[checkDateStr] > 0) { + setSelectedDay(checkDateStr); + found = true; + break; + } + currentDate.setUTCDate(currentDate.getUTCDate() + 1); + } + if (!found) { + toast.error(t("message.no-more-memos")); + } + }; + + const handlePrevMonth = () => { + if (!selectedDay) return; + const currentMonth = new Date(selectedDay); + currentMonth.setUTCMonth(currentMonth.getUTCMonth() - 1); + + const prevMonthStr = currentMonth.toISOString().substring(0, 7); + const datesWithData = Object.keys(activityStats).filter(date => date.startsWith(prevMonthStr)).sort(); + if (datesWithData.length > 0) { + setSelectedDay(datesWithData[0]); + } else { + handlePrevDay() + } + }; + const handleNextMonth = () => { + if (!selectedDay) return; + const currentMonth = new Date(selectedDay); + currentMonth.setUTCMonth(currentMonth.getUTCMonth() + 1); + const nextMonthStr = currentMonth.toISOString().substring(0, 7); + const datesWithData = Object.keys(activityStats).filter(date => date.startsWith(nextMonthStr)).sort(); + if (datesWithData.length > 0) { + setSelectedDay(datesWithData[0]); + } else { + const currentMonthStr = selectedDay.substring(0, 7); + const datesInCurrentMonth = Object.keys(activityStats).filter(date => date.startsWith(currentMonthStr)).sort(); + if (datesInCurrentMonth.length > 0 && selectedDay !== datesInCurrentMonth[datesInCurrentMonth.length - 1]) { + setSelectedDay(datesInCurrentMonth[datesInCurrentMonth.length - 1]); + } else { + toast.error(t("message.no-more-memos")); + } + } + }; + useEffect(() => { nextPageTokenRef.current = undefined; memoList.reset(); @@ -139,6 +221,23 @@ const Timeline = () => {
+ { selectedDay && ( +
+ handlePrevMonth()}> + + + handlePrevDay()}> + + + handleNextDay()}> + + + handleNextMonth()}> + + +
+ )} + handleNewMemo()}> @@ -147,17 +246,33 @@ const Timeline = () => {
- {groupedByMonth.map((group, index) => ( + {activityStats && Object.keys(activityStats).length > 0 && groupedByMonth.map((group, index) => (
-
+
{new Date(group.month).toLocaleString(i18n.language, { month: "short", timeZone: "UTC" })} + {selectedDay ? ( + + {new Date(selectedDay).toLocaleDateString(i18n.language, { + month: 'long', + day: 'numeric', + timeZone: "UTC" + })} + + ) : ( + + {new Date().toLocaleDateString(i18n.language, { + month: 'long', + timeZone: "UTC" + })} + + )} {new Date(group.month).getUTCFullYear()}
- setSelectedDay(date)} /> + handleDateClick(date)} selectedDate={selectedDay} />