+
{label}
))}
@@ -61,4 +62,4 @@ export const CompactMonthCalendar = memo((props: CompactMonthCalendarProps) => {
);
});
-CompactMonthCalendar.displayName = "CompactMonthCalendar";
+MonthCalendar.displayName = "MonthCalendar";
diff --git a/web/src/components/ActivityCalendar/MonthCard.tsx b/web/src/components/ActivityCalendar/MonthCard.tsx
deleted file mode 100644
index aaea282e1..000000000
--- a/web/src/components/ActivityCalendar/MonthCard.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-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/YearCalendar.tsx b/web/src/components/ActivityCalendar/YearCalendar.tsx
new file mode 100644
index 000000000..aa875aa82
--- /dev/null
+++ b/web/src/components/ActivityCalendar/YearCalendar.tsx
@@ -0,0 +1,93 @@
+import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
+import { useMemo } from "react";
+import {
+ calculateYearMaxCount,
+ filterDataByYear,
+ generateMonthsForYear,
+ getMonthLabel,
+ MonthCalendar,
+} from "@/components/ActivityCalendar";
+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 type { YearCalendarProps } from "./types";
+
+export const YearCalendar = ({ selectedYear, data, onYearChange, onDateClick, className }: YearCalendarProps) => {
+ const t = useTranslate();
+ const currentYear = useMemo(() => new Date().getFullYear(), []);
+ 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();
+ const isCurrentYear = selectedYear === currentYear;
+
+ const handlePrevYear = () => canGoPrev && onYearChange(selectedYear - 1);
+ const handleNextYear = () => canGoNext && onYearChange(selectedYear + 1);
+ const handleToday = () => onYearChange(currentYear);
+
+ return (
+
+
+
{selectedYear}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {months.map((month) => (
+
+
{getMonthLabel(month)}
+
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/web/src/components/ActivityCalendar/constants.ts b/web/src/components/ActivityCalendar/constants.ts
index 1908dc73f..e5cab3f08 100644
--- a/web/src/components/ActivityCalendar/constants.ts
+++ b/web/src/components/ActivityCalendar/constants.ts
@@ -13,15 +13,23 @@ export const INTENSITY_THRESHOLDS = {
MINIMAL: 0,
} as const;
+export const CELL_STYLES = {
+ HIGH: "bg-primary text-primary-foreground shadow-sm",
+ MEDIUM: "bg-primary/80 text-primary-foreground shadow-sm",
+ LOW: "bg-primary/60 text-primary-foreground shadow-sm",
+ MINIMAL: "bg-primary/40 text-foreground",
+ EMPTY: "bg-secondary/30 text-muted-foreground hover:bg-secondary/50",
+} as const;
+
export const SMALL_CELL_SIZE = {
- font: "text-[10px]",
- dimensions: "max-w-6 max-h-6",
- borderRadius: "rounded-sm",
- gap: "gap-px",
+ font: "text-xs",
+ dimensions: "w-8 h-8 mx-auto",
+ borderRadius: "rounded-md",
+ gap: "gap-1",
} as const;
export const DEFAULT_CELL_SIZE = {
font: "text-xs",
- borderRadius: "rounded",
- gap: "gap-0.5",
+ borderRadius: "rounded-md",
+ gap: "gap-1.5",
} as const;
diff --git a/web/src/components/ActivityCalendar/shared.ts b/web/src/components/ActivityCalendar/hooks.ts
similarity index 52%
rename from web/src/components/ActivityCalendar/shared.ts
rename to web/src/components/ActivityCalendar/hooks.ts
index 5b9aa5ef7..e3d2cfb79 100644
--- a/web/src/components/ActivityCalendar/shared.ts
+++ b/web/src/components/ActivityCalendar/hooks.ts
@@ -2,8 +2,6 @@ import dayjs from "dayjs";
import { useMemo } from "react";
import { useTranslate } from "@/utils/i18n";
-export type TranslateFunction = ReturnType
;
-
export const useWeekdayLabels = () => {
const t = useTranslate();
return useMemo(() => [t("days.sun"), t("days.mon"), t("days.tue"), t("days.wed"), t("days.thu"), t("days.fri"), t("days.sat")], [t]);
@@ -12,15 +10,3 @@ export const useWeekdayLabels = () => {
export const useTodayDate = () => {
return dayjs().format("YYYY-MM-DD");
};
-
-export const getTooltipText = (count: number, date: string, t: TranslateFunction): string => {
- if (count === 0) {
- return date;
- }
-
- return t("memo.count-memos-in-date", {
- count,
- memos: count === 1 ? t("common.memo") : t("common.memos"),
- date,
- }).toLowerCase();
-};
diff --git a/web/src/components/ActivityCalendar/index.ts b/web/src/components/ActivityCalendar/index.ts
index 121b21684..2e9454d46 100644
--- a/web/src/components/ActivityCalendar/index.ts
+++ b/web/src/components/ActivityCalendar/index.ts
@@ -1,26 +1,4 @@
-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 {
- calculateYearMaxCount,
- filterDataByYear,
- generateMonthsForYear,
- getCellIntensityClass,
- getMonthLabel,
- hasActivityData,
-} from "./utils";
+export * from "./MonthCalendar";
+export * from "./types";
+export * from "./utils";
+export * from "./YearCalendar";
diff --git a/web/src/components/ActivityCalendar/types.ts b/web/src/components/ActivityCalendar/types.ts
index aaf9dba16..00880fbfb 100644
--- a/web/src/components/ActivityCalendar/types.ts
+++ b/web/src/components/ActivityCalendar/types.ts
@@ -20,23 +20,16 @@ export interface CalendarMatrixResult {
maxCount: number;
}
-export interface CompactMonthCalendarProps {
+export interface MonthCalendarProps {
month: string;
data: Record;
maxCount: number;
size?: CalendarSize;
onClick?: (date: string) => void;
-}
-
-export interface MonthCardProps {
- month: string;
- data: Record;
- maxCount: number;
- onClick?: (date: string) => void;
className?: string;
}
-export interface CalendarPopoverProps {
+export interface YearCalendarProps {
selectedYear: number;
data: Record;
onYearChange: (year: number) => void;
diff --git a/web/src/components/ActivityCalendar/useCalendarMatrix.ts b/web/src/components/ActivityCalendar/useCalendar.ts
similarity index 85%
rename from web/src/components/ActivityCalendar/useCalendarMatrix.ts
rename to web/src/components/ActivityCalendar/useCalendar.ts
index 0c8dab832..783767e44 100644
--- a/web/src/components/ActivityCalendar/useCalendarMatrix.ts
+++ b/web/src/components/ActivityCalendar/useCalendar.ts
@@ -45,6 +45,9 @@ const calculateCalendarBoundaries = (monthStart: dayjs.Dayjs, weekStartDayOffset
return { calendarStart, dayCount };
};
+/**
+ * Generates a matrix of calendar days for a given month, handling week alignment and data mapping.
+ */
export const useCalendarMatrix = ({
month,
data,
@@ -54,16 +57,20 @@ export const useCalendarMatrix = ({
selectedDate,
}: UseCalendarMatrixParams): CalendarMatrixResult => {
return useMemo(() => {
+ // Determine the start of the month and its formatted key (YYYY-MM)
const monthStart = dayjs(month).startOf("month");
const monthKey = monthStart.format("YYYY-MM");
+ // Rotate week labels based on the user's preferred start of the week
const rotatedWeekDays = weekDays.slice(weekStartDayOffset).concat(weekDays.slice(0, weekStartDayOffset));
+ // Calculate the start and end dates for the calendar grid to ensure full weeks
const { calendarStart, dayCount } = calculateCalendarBoundaries(monthStart, weekStartDayOffset);
const weeks: CalendarMatrixResult["weeks"] = [];
let maxCount = 0;
+ // Iterate through each day in the calendar grid
for (let index = 0; index < dayCount; index += 1) {
const current = calendarStart.add(index, "day");
const weekIndex = Math.floor(index / DAYS_IN_WEEK);
@@ -72,6 +79,7 @@ export const useCalendarMatrix = ({
weeks[weekIndex] = { days: [] };
}
+ // Create the day cell object with data and status flags
const dayCell = createCalendarDayCell(current, monthKey, data, today, selectedDate);
weeks[weekIndex].days.push(dayCell);
maxCount = Math.max(maxCount, dayCell.count);
diff --git a/web/src/components/ActivityCalendar/utils.ts b/web/src/components/ActivityCalendar/utils.ts
index a5a20e089..0ce1029db 100644
--- a/web/src/components/ActivityCalendar/utils.ts
+++ b/web/src/components/ActivityCalendar/utils.ts
@@ -1,22 +1,25 @@
import dayjs from "dayjs";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
-import { INTENSITY_THRESHOLDS, MIN_COUNT, MONTHS_IN_YEAR } from "./constants";
+import { useTranslate } from "@/utils/i18n";
+import { CELL_STYLES, INTENSITY_THRESHOLDS, MIN_COUNT, MONTHS_IN_YEAR } from "./constants";
import type { CalendarDayCell } from "./types";
dayjs.extend(isSameOrAfter);
dayjs.extend(isSameOrBefore);
+export type TranslateFunction = ReturnType;
+
export const getCellIntensityClass = (day: CalendarDayCell, maxCount: number): string => {
if (!day.isCurrentMonth || day.count === 0) {
- return "bg-transparent";
+ return CELL_STYLES.EMPTY;
}
const ratio = day.count / maxCount;
- if (ratio > INTENSITY_THRESHOLDS.HIGH) return "bg-primary text-primary-foreground border-primary";
- if (ratio > INTENSITY_THRESHOLDS.MEDIUM) return "bg-primary/80 text-primary-foreground border-primary/90";
- if (ratio > INTENSITY_THRESHOLDS.LOW) return "bg-primary/60 text-primary-foreground border-primary/70";
- return "bg-primary/40 text-primary";
+ if (ratio > INTENSITY_THRESHOLDS.HIGH) return CELL_STYLES.HIGH;
+ if (ratio > INTENSITY_THRESHOLDS.MEDIUM) return CELL_STYLES.MEDIUM;
+ if (ratio > INTENSITY_THRESHOLDS.LOW) return CELL_STYLES.LOW;
+ return CELL_STYLES.MINIMAL;
};
export const generateMonthsForYear = (year: number): string[] => {
@@ -32,7 +35,7 @@ export const calculateYearMaxCount = (data: Record): number => {
};
export const getMonthLabel = (month: string): string => {
- return dayjs(month).format("MMM YYYY");
+ return dayjs(month).format("MMM");
};
export const filterDataByYear = (data: Record, year: number): Record => {
@@ -55,3 +58,15 @@ export const filterDataByYear = (data: Record, year: number): Re
export const hasActivityData = (data: Record): boolean => {
return Object.values(data).some((count) => count > 0);
};
+
+export const getTooltipText = (count: number, date: string, t: TranslateFunction): string => {
+ if (count === 0) {
+ return date;
+ }
+
+ return t("memo.count-memos-in-date", {
+ count,
+ memos: count === 1 ? t("common.memo") : t("common.memos"),
+ date,
+ }).toLowerCase();
+};
diff --git a/web/src/components/MemoExplorer/MemoExplorer.tsx b/web/src/components/MemoExplorer/MemoExplorer.tsx
index 967d9cf23..056352349 100644
--- a/web/src/components/MemoExplorer/MemoExplorer.tsx
+++ b/web/src/components/MemoExplorer/MemoExplorer.tsx
@@ -13,7 +13,6 @@ export interface MemoExplorerFeatures {
statistics?: boolean;
shortcuts?: boolean;
tags?: boolean;
- statisticsContext?: MemoExplorerContext;
}
interface Props {
@@ -32,7 +31,6 @@ const getDefaultFeatures = (context: MemoExplorerContext): MemoExplorerFeatures
statistics: true,
shortcuts: false, // Global explore doesn't use shortcuts
tags: true,
- statisticsContext: "explore",
};
case "archived":
return {
@@ -40,7 +38,6 @@ const getDefaultFeatures = (context: MemoExplorerContext): MemoExplorerFeatures
statistics: true,
shortcuts: false, // Archived doesn't typically use shortcuts
tags: true,
- statisticsContext: "archived",
};
case "profile":
return {
@@ -48,7 +45,6 @@ const getDefaultFeatures = (context: MemoExplorerContext): MemoExplorerFeatures
statistics: true,
shortcuts: false, // Profile view doesn't use shortcuts
tags: true,
- statisticsContext: "profile",
};
case "home":
default:
@@ -57,7 +53,6 @@ const getDefaultFeatures = (context: MemoExplorerContext): MemoExplorerFeatures
statistics: true,
shortcuts: true,
tags: true,
- statisticsContext: "home",
};
}
};
@@ -81,7 +76,7 @@ const MemoExplorer = (props: Props) => {
>
{features.search && }
- {features.statistics && }
+ {features.statistics && }
{features.shortcuts && currentUser && }
{features.tags && }
diff --git a/web/src/components/StatisticsView/MonthNavigator.tsx b/web/src/components/StatisticsView/MonthNavigator.tsx
index c1dc292b9..fe4eadb46 100644
--- a/web/src/components/StatisticsView/MonthNavigator.tsx
+++ b/web/src/components/StatisticsView/MonthNavigator.tsx
@@ -1,24 +1,17 @@
-import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
+import { ChevronDownIcon, 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 { YearCalendar } from "@/components/ActivityCalendar";
+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 }: MonthNavigatorProps) => {
- const currentUser = useCurrentUser();
+export const MonthNavigator = ({ visibleMonth, onMonthChange, activityStats }: MonthNavigatorProps) => {
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(addMonths(visibleMonth, -1));
};
@@ -37,28 +30,33 @@ export const MonthNavigator = ({ visibleMonth, onMonthChange }: MonthNavigatorPr
};
return (
-
-
-
-
+
+