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" })}
+
+
+
+
+
+