From ea3371badb3a97d6bd80b4e363111e4d675d7c12 Mon Sep 17 00:00:00 2001 From: Johnny Date: Sun, 28 Dec 2025 19:59:36 +0800 Subject: [PATCH] chore: add ActivityCalendar components --- .../ActivityCalendar/CalendarHeader.tsx | 73 +++++++++ .../ActivityCalendar/CalendarPopover.tsx | 32 ++++ .../components/ActivityCalendar/MonthCard.tsx | 17 +++ .../components/ActivityCalendar/constants.ts | 3 + web/src/components/ActivityCalendar/index.ts | 5 + web/src/components/ActivityCalendar/types.ts | 16 ++ web/src/components/Navigation.tsx | 10 +- .../StatisticsView/MonthNavigator.tsx | 48 +++++- web/src/lib/calendar-utils.ts | 23 +++ web/src/pages/Calendar.tsx | 144 ------------------ web/src/router/index.tsx | 9 -- web/src/router/routes.ts | 1 - 12 files changed, 212 insertions(+), 169 deletions(-) create mode 100644 web/src/components/ActivityCalendar/CalendarHeader.tsx create mode 100644 web/src/components/ActivityCalendar/CalendarPopover.tsx create mode 100644 web/src/components/ActivityCalendar/MonthCard.tsx create mode 100644 web/src/lib/calendar-utils.ts delete mode 100644 web/src/pages/Calendar.tsx diff --git a/web/src/components/ActivityCalendar/CalendarHeader.tsx b/web/src/components/ActivityCalendar/CalendarHeader.tsx new file mode 100644 index 000000000..154adbbb8 --- /dev/null +++ b/web/src/components/ActivityCalendar/CalendarHeader.tsx @@ -0,0 +1,73 @@ +import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; +import { useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { useTranslate } from "@/utils/i18n"; + +interface CalendarHeaderProps { + selectedYear: number; + onYearChange: (year: number) => void; + canGoPrev: boolean; + canGoNext: boolean; +} + +export const CalendarHeader = ({ selectedYear, onYearChange, canGoPrev, canGoNext }: CalendarHeaderProps) => { + const t = useTranslate(); + const currentYear = useMemo(() => new Date().getFullYear(), []); + const isCurrentYear = selectedYear === currentYear; + + const handlePrevYear = () => { + if (canGoPrev) { + onYearChange(selectedYear - 1); + } + }; + + const handleNextYear = () => { + if (canGoNext) { + onYearChange(selectedYear + 1); + } + }; + + const handleToday = () => { + onYearChange(currentYear); + }; + + return ( +
+

{selectedYear}

+ +
+ + + + + +
+
+ ); +}; diff --git a/web/src/components/ActivityCalendar/CalendarPopover.tsx b/web/src/components/ActivityCalendar/CalendarPopover.tsx new file mode 100644 index 000000000..9abdb9bd3 --- /dev/null +++ b/web/src/components/ActivityCalendar/CalendarPopover.tsx @@ -0,0 +1,32 @@ +import { useMemo } from "react"; +import { calculateYearMaxCount, filterDataByYear, generateMonthsForYear } from "@/components/ActivityCalendar"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { CalendarHeader } from "./CalendarHeader"; +import { getMaxYear, MIN_YEAR } from "./constants"; +import { MonthCard } from "./MonthCard"; +import type { CalendarPopoverProps } from "./types"; + +export const CalendarPopover = ({ selectedYear, data, onYearChange, onDateClick, className }: CalendarPopoverProps) => { + const yearData = useMemo(() => filterDataByYear(data, selectedYear), [data, selectedYear]); + const months = useMemo(() => generateMonthsForYear(selectedYear), [selectedYear]); + const yearMaxCount = useMemo(() => calculateYearMaxCount(yearData), [yearData]); + const canGoPrev = selectedYear > MIN_YEAR; + const canGoNext = selectedYear < getMaxYear(); + + return ( +
+ + + +
+
+ {months.map((month) => ( + + ))} +
+
+
+
+ ); +}; diff --git a/web/src/components/ActivityCalendar/MonthCard.tsx b/web/src/components/ActivityCalendar/MonthCard.tsx new file mode 100644 index 000000000..aaea282e1 --- /dev/null +++ b/web/src/components/ActivityCalendar/MonthCard.tsx @@ -0,0 +1,17 @@ +import { CompactMonthCalendar, getMonthLabel } from "@/components/ActivityCalendar"; +import { cn } from "@/lib/utils"; +import type { MonthCardProps } from "./types"; + +export const MonthCard = ({ month, data, maxCount, onClick, className }: MonthCardProps) => { + return ( +
+
{getMonthLabel(month)}
+ +
+ ); +}; diff --git a/web/src/components/ActivityCalendar/constants.ts b/web/src/components/ActivityCalendar/constants.ts index 0c24e882c..1908dc73f 100644 --- a/web/src/components/ActivityCalendar/constants.ts +++ b/web/src/components/ActivityCalendar/constants.ts @@ -3,6 +3,9 @@ export const MONTHS_IN_YEAR = 12; export const WEEKEND_DAYS = [0, 6] as const; export const MIN_COUNT = 1; +export const MIN_YEAR = 2000; +export const getMaxYear = () => new Date().getFullYear() + 1; + export const INTENSITY_THRESHOLDS = { HIGH: 0.75, MEDIUM: 0.5, diff --git a/web/src/components/ActivityCalendar/index.ts b/web/src/components/ActivityCalendar/index.ts index bb1fd2055..121b21684 100644 --- a/web/src/components/ActivityCalendar/index.ts +++ b/web/src/components/ActivityCalendar/index.ts @@ -1,14 +1,19 @@ export { ActivityCalendar as default } from "./ActivityCalendar"; export { CalendarCell, type CalendarCellProps } from "./CalendarCell"; +export { CalendarHeader } from "./CalendarHeader"; +export { CalendarPopover } from "./CalendarPopover"; export { CompactMonthCalendar } from "./CompactMonthCalendar"; export * from "./constants"; +export { MonthCard } from "./MonthCard"; export { getTooltipText, type TranslateFunction, useTodayDate, useWeekdayLabels } from "./shared"; export type { CalendarDayCell, CalendarDayRow, CalendarMatrixResult, + CalendarPopoverProps, CalendarSize, CompactMonthCalendarProps, + MonthCardProps, } from "./types"; export { type UseCalendarMatrixParams, useCalendarMatrix } from "./useCalendarMatrix"; export { diff --git a/web/src/components/ActivityCalendar/types.ts b/web/src/components/ActivityCalendar/types.ts index 9b6366d12..aaf9dba16 100644 --- a/web/src/components/ActivityCalendar/types.ts +++ b/web/src/components/ActivityCalendar/types.ts @@ -27,3 +27,19 @@ export interface CompactMonthCalendarProps { size?: CalendarSize; onClick?: (date: string) => void; } + +export interface MonthCardProps { + month: string; + data: Record; + maxCount: number; + onClick?: (date: string) => void; + className?: string; +} + +export interface CalendarPopoverProps { + selectedYear: number; + data: Record; + onYearChange: (year: number) => void; + onDateClick: (date: string) => void; + className?: string; +} diff --git a/web/src/components/Navigation.tsx b/web/src/components/Navigation.tsx index c5c18888a..3778459bc 100644 --- a/web/src/components/Navigation.tsx +++ b/web/src/components/Navigation.tsx @@ -1,4 +1,4 @@ -import { BellIcon, CalendarIcon, EarthIcon, LibraryIcon, PaperclipIcon, UserCircleIcon } from "lucide-react"; +import { BellIcon, EarthIcon, LibraryIcon, PaperclipIcon, UserCircleIcon } from "lucide-react"; import { NavLink } from "react-router-dom"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import useCurrentUser from "@/hooks/useCurrentUser"; @@ -34,12 +34,6 @@ const Navigation = (props: Props) => { title: t("common.memos"), icon: , }; - const calendarNavLink: NavLinkItem = { - id: "header-calendar", - path: Routes.CALENDAR, - title: t("common.calendar"), - icon: , - }; const exploreNavLink: NavLinkItem = { id: "header-explore", path: Routes.EXPLORE, @@ -76,7 +70,7 @@ const Navigation = (props: Props) => { }; const navLinks: NavLinkItem[] = currentUser - ? [homeNavLink, calendarNavLink, exploreNavLink, attachmentsNavLink, inboxNavLink] + ? [homeNavLink, exploreNavLink, attachmentsNavLink, inboxNavLink] : [exploreNavLink, signInNavLink]; return ( diff --git a/web/src/components/StatisticsView/MonthNavigator.tsx b/web/src/components/StatisticsView/MonthNavigator.tsx index 0533fd1ce..c1dc292b9 100644 --- a/web/src/components/StatisticsView/MonthNavigator.tsx +++ b/web/src/components/StatisticsView/MonthNavigator.tsx @@ -1,24 +1,58 @@ -import dayjs from "dayjs"; import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; +import { useState } from "react"; +import { CalendarPopover } from "@/components/ActivityCalendar"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import { useFilteredMemoStats } from "@/hooks/useFilteredMemoStats"; 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 }: MonthNavigatorProps) => { - const currentMonth = dayjs(visibleMonth).toDate(); + const currentUser = useCurrentUser(); + const [isOpen, setIsOpen] = useState(false); + const currentMonth = new Date(visibleMonth); + const currentYear = getYearFromDate(visibleMonth); + const currentMonthNum = getMonthFromDate(visibleMonth); + + const { statistics } = useFilteredMemoStats({ + userName: currentUser?.name, + }); const handlePrevMonth = () => { - onMonthChange(dayjs(visibleMonth).subtract(1, "month").format("YYYY-MM")); + onMonthChange(addMonths(visibleMonth, -1)); }; const handleNextMonth = () => { - onMonthChange(dayjs(visibleMonth).add(1, "month").format("YYYY-MM")); + onMonthChange(addMonths(visibleMonth, 1)); + }; + + const handleDateClick = (date: string) => { + onMonthChange(formatMonth(date)); + setIsOpen(false); + }; + + const handleYearChange = (year: number) => { + onMonthChange(setYearAndMonth(year, currentMonthNum)); }; return (
- - {currentMonth.toLocaleString(i18n.language, { year: "numeric", month: "long" })} - + + + + {currentMonth.toLocaleString(i18n.language, { year: "numeric", month: "long" })} + + + + + +
- - - - -
-
- - {loading ? ( -
-
-
-

Loading calendar...

-
-
- ) : ( - -
-
- {months.map((month) => ( -
-
{getMonthLabel(month)}
- -
- ))} -
-
-
- )} -
- - - ); -}; - -export default Calendar; diff --git a/web/src/router/index.tsx b/web/src/router/index.tsx index 655675f71..5bf27269f 100644 --- a/web/src/router/index.tsx +++ b/web/src/router/index.tsx @@ -9,7 +9,6 @@ import Home from "@/pages/Home"; const AdminSignIn = lazy(() => import("@/pages/AdminSignIn")); const Archived = lazy(() => import("@/pages/Archived")); const AuthCallback = lazy(() => import("@/pages/AuthCallback")); -const Calendar = lazy(() => import("@/pages/Calendar")); const Explore = lazy(() => import("@/pages/Explore")); const Inboxes = lazy(() => import("@/pages/Inboxes")); const MemoDetail = lazy(() => import("@/pages/MemoDetail")); @@ -115,14 +114,6 @@ const router = createBrowserRouter([ ), }, - { - path: Routes.CALENDAR, - element: ( - }> - - - ), - }, { path: Routes.INBOX, element: ( diff --git a/web/src/router/routes.ts b/web/src/router/routes.ts index 8f0058b57..74502cc2a 100644 --- a/web/src/router/routes.ts +++ b/web/src/router/routes.ts @@ -1,7 +1,6 @@ export const ROUTES = { ROOT: "/", ATTACHMENTS: "/attachments", - CALENDAR: "/calendar", INBOX: "/inbox", ARCHIVED: "/archived", SETTING: "/setting",