diff --git a/web/src/components/ActivityCalendar/MonthCalendar.tsx b/web/src/components/ActivityCalendar/MonthCalendar.tsx index 9869655c9..e57a6a127 100644 --- a/web/src/components/ActivityCalendar/MonthCalendar.tsx +++ b/web/src/components/ActivityCalendar/MonthCalendar.tsx @@ -1,20 +1,43 @@ -import { memo } from "react"; +import { memo, useMemo } from "react"; import { useInstance } from "@/contexts/InstanceContext"; import { cn } from "@/lib/utils"; import { useTranslate } from "@/utils/i18n"; import { CalendarCell } from "./CalendarCell"; import { useTodayDate, useWeekdayLabels } from "./hooks"; -import type { MonthCalendarProps } from "./types"; +import type { CalendarSize, MonthCalendarProps } from "./types"; import { useCalendarMatrix } from "./useCalendar"; import { getTooltipText } from "./utils"; +const GRID_STYLES: Record = { + small: { gap: "gap-1.5", headerText: "text-[10px]" }, + default: { gap: "gap-2", headerText: "text-xs" }, +}; + +interface WeekdayHeaderProps { + weekDays: string[]; + size: CalendarSize; +} + +const WeekdayHeader = memo(({ weekDays, size }: WeekdayHeaderProps) => ( +
+ {weekDays.map((label, index) => ( +
+ {label} +
+ ))} +
+)); +WeekdayHeader.displayName = "WeekdayHeader"; + export const MonthCalendar = memo((props: MonthCalendarProps) => { const { month, data, maxCount, size = "default", onClick, className } = props; const t = useTranslate(); const { generalSetting } = useInstance(); - - const weekStartDayOffset = generalSetting.weekStartDayOffset; - const today = useTodayDate(); const weekDays = useWeekdayLabels(); @@ -22,41 +45,29 @@ export const MonthCalendar = memo((props: MonthCalendarProps) => { month, data, weekDays, - weekStartDayOffset, + weekStartDayOffset: generalSetting.weekStartDayOffset, today, selectedDate: "", }); - const gridGap = size === "small" ? "gap-x-3 gap-y-3" : "gap-x-3.5 gap-y-3.5"; + const flatDays = useMemo(() => weeks.flatMap((week) => week.days), [weeks]); return ( -
-
- {rotatedWeekDays.map((label, index) => ( -
- {label} -
+
+ + +
+ {flatDays.map((day) => ( + ))}
- -
- {weeks.map((week, weekIndex) => - week.days.map((day, dayIndex) => { - const tooltipText = getTooltipText(day.count, day.date, t); - - return ( - - ); - }), - )} -
); }); diff --git a/web/src/components/ActivityCalendar/YearCalendar.tsx b/web/src/components/ActivityCalendar/YearCalendar.tsx index c35b13f9e..38ace4f48 100644 --- a/web/src/components/ActivityCalendar/YearCalendar.tsx +++ b/web/src/components/ActivityCalendar/YearCalendar.tsx @@ -1,21 +1,90 @@ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; -import { useMemo } from "react"; -import { - calculateYearMaxCount, - filterDataByYear, - generateMonthsForYear, - getMonthLabel, - MonthCalendar, -} from "@/components/ActivityCalendar"; +import { memo, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { TooltipProvider } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { useTranslate } from "@/utils/i18n"; import { getMaxYear, MIN_YEAR } from "./constants"; +import { MonthCalendar } from "./MonthCalendar"; import type { YearCalendarProps } from "./types"; +import { calculateYearMaxCount, filterDataByYear, generateMonthsForYear, getMonthLabel } from "./utils"; -export const YearCalendar = ({ selectedYear, data, onYearChange, onDateClick, className }: YearCalendarProps) => { +interface YearNavigationProps { + selectedYear: number; + currentYear: number; + onPrev: () => void; + onNext: () => void; + onToday: () => void; + canGoPrev: boolean; + canGoNext: boolean; +} + +const YearNavigation = memo(({ selectedYear, currentYear, onPrev, onNext, onToday, canGoPrev, canGoNext }: YearNavigationProps) => { const t = useTranslate(); + const isCurrentYear = selectedYear === currentYear; + + return ( +
+

{selectedYear}

+ + +
+ ); +}); +YearNavigation.displayName = "YearNavigation"; + +interface MonthCardProps { + month: string; + data: Record; + maxCount: number; + onDateClick: (date: string) => void; +} + +const MonthCard = memo(({ month, data, maxCount, onDateClick }: MonthCardProps) => ( +
+
{getMonthLabel(month)}
+ +
+)); +MonthCard.displayName = "MonthCard"; + +export const YearCalendar = memo(({ selectedYear, data, onYearChange, onDateClick, className }: YearCalendarProps) => { const currentYear = useMemo(() => new Date().getFullYear(), []); const yearData = useMemo(() => filterDataByYear(data, selectedYear), [data, selectedYear]); const months = useMemo(() => generateMonthsForYear(selectedYear), [selectedYear]); @@ -23,75 +92,28 @@ export const YearCalendar = ({ selectedYear, data, onYearChange, onDateClick, cl const canGoPrev = selectedYear > MIN_YEAR; const canGoNext = selectedYear < getMaxYear(); - const isCurrentYear = selectedYear === currentYear; - - const handlePrevYear = () => canGoPrev && onYearChange(selectedYear - 1); - const handleNextYear = () => canGoNext && onYearChange(selectedYear + 1); - const handleToday = () => onYearChange(currentYear); return ( -
-
-
-

{selectedYear}

-
- -
- - - - - -
-
+
+ canGoPrev && onYearChange(selectedYear - 1)} + onNext={() => canGoNext && onYearChange(selectedYear + 1)} + onToday={() => onYearChange(currentYear)} + canGoPrev={canGoPrev} + canGoNext={canGoNext} + /> -
-
- {months.map((month) => ( -
-
- {getMonthLabel(month)} -
- -
- ))} -
+
+ {months.map((month) => ( + + ))}
-
+
); -}; +}); + +YearCalendar.displayName = "YearCalendar"; diff --git a/web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx b/web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx index 00e7e3f84..4a3822afa 100644 --- a/web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx +++ b/web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx @@ -18,28 +18,25 @@ interface Props { const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => { const t = useTranslate(); - const property = create(Memo_PropertySchema, memo.property || {}); - const hasSpecialProperty = property.hasLink || property.hasTaskList || property.hasCode || property.hasIncompleteTasks; - const shouldShowRelationGraph = memo.relations.filter((r) => r.type === MemoRelation_Type.REFERENCE).length > 0; const { mutate: updateMemo } = useUpdateMemo(); + const property = create(Memo_PropertySchema, memo.property || {}); + const hasSpecialProperty = property.hasLink || property.hasTaskList || property.hasCode; + const hasReferenceRelations = memo.relations.some((r) => r.type === MemoRelation_Type.REFERENCE); const handleUpdateTimestamp = (field: "createTime" | "updateTime", date: Date) => { - const timestamp = timestampFromDate(date); + const currentTimestamp = memo[field]; + const newTimestamp = timestampFromDate(date); + if (isEqual(currentTimestamp, newTimestamp)) { + return; + } updateMemo( { - update: { - name: memo.name, - [field]: timestamp, - }, + update: { name: memo.name, [field]: newTimestamp }, updateMask: [field === "createTime" ? "create_time" : "update_time"], }, { - onSuccess: () => { - toast.success("Updated successfully"); - }, - onError: (error) => { - toast.error(error.message); - }, + onSuccess: () => toast.success("Updated successfully"), + onError: (error) => toast.error(error.message), }, ); }; @@ -49,7 +46,7 @@ const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => { className={cn("relative w-full h-auto max-h-screen overflow-auto hide-scrollbar flex flex-col justify-start items-start", className)} >
- {shouldShowRelationGraph && ( + {hasReferenceRelations && (
@@ -58,16 +55,19 @@ const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => {
)} +

{t("common.created-at")}

handleUpdateTimestamp("createTime", date)} />
+ {!isEqual(memo.createTime, memo.updateTime) && (

{t("common.last-updated-at")}

handleUpdateTimestamp("updateTime", date)} />
)} + {hasSpecialProperty && (

{t("common.properties")}

@@ -93,6 +93,7 @@ const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => {
)} + {memo.tags.length > 0 && (
diff --git a/web/src/components/StatisticsView/MonthNavigator.tsx b/web/src/components/StatisticsView/MonthNavigator.tsx index 9bd5e6b89..b15f43f40 100644 --- a/web/src/components/StatisticsView/MonthNavigator.tsx +++ b/web/src/components/StatisticsView/MonthNavigator.tsx @@ -1,45 +1,56 @@ import dayjs from "dayjs"; import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; -import { useState } from "react"; +import { memo, useCallback, useMemo, useState } from "react"; import { YearCalendar } from "@/components/ActivityCalendar"; +import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import i18n from "@/i18n"; import { addMonths, formatMonth, getMonthFromDate, getYearFromDate, setYearAndMonth } from "@/lib/calendar-utils"; import type { MonthNavigatorProps } from "@/types/statistics"; -export const MonthNavigator = ({ visibleMonth, onMonthChange, activityStats }: MonthNavigatorProps) => { +export const MonthNavigator = memo(({ visibleMonth, onMonthChange, activityStats }: MonthNavigatorProps) => { const [isOpen, setIsOpen] = useState(false); - const currentMonth = dayjs(visibleMonth).toDate(); - const currentYear = getYearFromDate(visibleMonth); - const currentMonthNum = getMonthFromDate(visibleMonth); - const handlePrevMonth = () => { - onMonthChange(addMonths(visibleMonth, -1)); - }; + const { currentMonth, currentYear, currentMonthNum } = useMemo( + () => ({ + currentMonth: dayjs(visibleMonth).toDate(), + currentYear: getYearFromDate(visibleMonth), + currentMonthNum: getMonthFromDate(visibleMonth), + }), + [visibleMonth], + ); - const handleNextMonth = () => { - onMonthChange(addMonths(visibleMonth, 1)); - }; + const monthLabel = useMemo(() => currentMonth.toLocaleString(i18n.language, { year: "numeric", month: "long" }), [currentMonth]); - const handleDateClick = (date: string) => { - onMonthChange(formatMonth(date)); - setIsOpen(false); - }; + const handlePrevMonth = useCallback(() => onMonthChange(addMonths(visibleMonth, -1)), [visibleMonth, onMonthChange]); + const handleNextMonth = useCallback(() => onMonthChange(addMonths(visibleMonth, 1)), [visibleMonth, onMonthChange]); - const handleYearChange = (year: number) => { - onMonthChange(setYearAndMonth(year, currentMonthNum)); - }; + const handleDateClick = useCallback( + (date: string) => { + onMonthChange(formatMonth(date)); + setIsOpen(false); + }, + [onMonthChange], + ); + + const handleYearChange = useCallback( + (year: number) => onMonthChange(setYearAndMonth(year, currentMonthNum)), + [currentMonthNum, onMonthChange], + ); return ( -
+
- @@ -47,22 +58,29 @@ export const MonthNavigator = ({ visibleMonth, onMonthChange, activityStats }: M -
- - + -
-
+ + + + ); -}; +}); + +MonthNavigator.displayName = "MonthNavigator";