mirror of https://github.com/usememos/memos
chore: refactor ActivityCalendar to use a calendar matrix and improve cell rendering
parent
5011eb5d70
commit
56758f107c
@ -0,0 +1,76 @@
|
||||
import { memo } from "react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { CalendarDayCell } from "./types";
|
||||
import { getCellIntensityClass } from "./utils";
|
||||
|
||||
interface CalendarCellProps {
|
||||
day: CalendarDayCell;
|
||||
maxCount: number;
|
||||
tooltipText: string;
|
||||
onClick?: (date: string) => void;
|
||||
}
|
||||
|
||||
export const CalendarCell = memo((props: CalendarCellProps) => {
|
||||
const { day, maxCount, tooltipText, onClick } = props;
|
||||
|
||||
const handleClick = () => {
|
||||
if (day.count > 0 && onClick) {
|
||||
onClick(day.date);
|
||||
}
|
||||
};
|
||||
|
||||
const baseClasses =
|
||||
"w-full h-7 rounded-md border text-xs flex items-center justify-center text-center transition-transform duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-1 focus-visible:ring-offset-background select-none";
|
||||
const isInteractive = Boolean(onClick && day.count > 0);
|
||||
const ariaLabel = day.isSelected ? `${tooltipText} (selected)` : tooltipText;
|
||||
|
||||
if (!day.isCurrentMonth) {
|
||||
return (
|
||||
<div className={cn(baseClasses, "border-transparent text-muted-foreground/60 bg-transparent pointer-events-none opacity-80")}>
|
||||
{day.label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const intensityClass = getCellIntensityClass(day, maxCount);
|
||||
|
||||
const buttonClasses = cn(
|
||||
baseClasses,
|
||||
"border-transparent text-muted-foreground",
|
||||
day.isToday && "border-border",
|
||||
day.isSelected && "border-border font-medium",
|
||||
day.isWeekend && "text-muted-foreground/80",
|
||||
intensityClass,
|
||||
isInteractive ? "cursor-pointer hover:scale-105" : "cursor-default",
|
||||
);
|
||||
|
||||
const button = (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
tabIndex={isInteractive ? 0 : -1}
|
||||
aria-label={ariaLabel}
|
||||
aria-current={day.isToday ? "date" : undefined}
|
||||
aria-disabled={!isInteractive}
|
||||
className={buttonClasses}
|
||||
>
|
||||
{day.label}
|
||||
</button>
|
||||
);
|
||||
|
||||
if (!tooltipText) {
|
||||
return button;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>{tooltipText}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
CalendarCell.displayName = "CalendarCell";
|
||||
@ -0,0 +1,19 @@
|
||||
export interface CalendarDayCell {
|
||||
date: string;
|
||||
label: number;
|
||||
count: number;
|
||||
isCurrentMonth: boolean;
|
||||
isToday: boolean;
|
||||
isSelected: boolean;
|
||||
isWeekend: boolean;
|
||||
}
|
||||
|
||||
export interface CalendarDayRow {
|
||||
days: CalendarDayCell[];
|
||||
}
|
||||
|
||||
export interface CalendarMatrixResult {
|
||||
weeks: CalendarDayRow[];
|
||||
weekDays: string[];
|
||||
maxCount: number;
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useMemo } from "react";
|
||||
import type { CalendarDayCell, CalendarMatrixResult } from "./types";
|
||||
|
||||
interface UseCalendarMatrixParams {
|
||||
month: string;
|
||||
data: Record<string, number>;
|
||||
weekDays: string[];
|
||||
weekStartDayOffset: number;
|
||||
today: string;
|
||||
selectedDate: string;
|
||||
}
|
||||
|
||||
export const useCalendarMatrix = ({
|
||||
month,
|
||||
data,
|
||||
weekDays,
|
||||
weekStartDayOffset,
|
||||
today,
|
||||
selectedDate,
|
||||
}: UseCalendarMatrixParams): CalendarMatrixResult => {
|
||||
return useMemo(() => {
|
||||
const monthStart = dayjs(month).startOf("month");
|
||||
const monthEnd = monthStart.endOf("month");
|
||||
const monthKey = monthStart.format("YYYY-MM");
|
||||
|
||||
const orderedWeekDays = weekDays.slice(weekStartDayOffset).concat(weekDays.slice(0, weekStartDayOffset));
|
||||
|
||||
const startOffset = (monthStart.day() - weekStartDayOffset + 7) % 7;
|
||||
const endOffset = (weekStartDayOffset + 6 - monthEnd.day() + 7) % 7;
|
||||
|
||||
const calendarStart = monthStart.subtract(startOffset, "day");
|
||||
const calendarEnd = monthEnd.add(endOffset, "day");
|
||||
const dayCount = calendarEnd.diff(calendarStart, "day") + 1;
|
||||
|
||||
const weeks: CalendarMatrixResult["weeks"] = [];
|
||||
let maxCount = 0;
|
||||
|
||||
for (let index = 0; index < dayCount; index += 1) {
|
||||
const current = calendarStart.add(index, "day");
|
||||
const isoDate = current.format("YYYY-MM-DD");
|
||||
const weekIndex = Math.floor(index / 7);
|
||||
|
||||
if (!weeks[weekIndex]) {
|
||||
weeks[weekIndex] = { days: [] };
|
||||
}
|
||||
|
||||
const isCurrentMonth = current.format("YYYY-MM") === monthKey;
|
||||
const count = data[isoDate] ?? 0;
|
||||
|
||||
const dayCell: CalendarDayCell = {
|
||||
date: isoDate,
|
||||
label: current.date(),
|
||||
count,
|
||||
isCurrentMonth,
|
||||
isToday: isoDate === today,
|
||||
isSelected: isoDate === selectedDate,
|
||||
isWeekend: [0, 6].includes(current.day()),
|
||||
};
|
||||
|
||||
weeks[weekIndex].days.push(dayCell);
|
||||
maxCount = Math.max(maxCount, count);
|
||||
}
|
||||
|
||||
return {
|
||||
weeks,
|
||||
weekDays: orderedWeekDays,
|
||||
maxCount: Math.max(maxCount, 1),
|
||||
};
|
||||
}, [month, data, weekDays, weekStartDayOffset, today, selectedDate]);
|
||||
};
|
||||
@ -0,0 +1,13 @@
|
||||
import type { CalendarDayCell } from "./types";
|
||||
|
||||
export const getCellIntensityClass = (day: CalendarDayCell, maxCount: number): string => {
|
||||
if (!day.isCurrentMonth || day.count === 0 || maxCount <= 0) {
|
||||
return "bg-transparent";
|
||||
}
|
||||
|
||||
const ratio = day.count / maxCount;
|
||||
if (ratio > 0.75) return "bg-primary text-primary-foreground border-primary";
|
||||
if (ratio > 0.5) return "bg-primary/80 text-primary-foreground border-primary/90";
|
||||
if (ratio > 0.25) return "bg-primary/60 text-primary-foreground border-primary/70";
|
||||
return "bg-primary/40 text-primary";
|
||||
};
|
||||
Loading…
Reference in New Issue