mirror of https://github.com/usememos/memos.git
chore: prevent unnecessary API calls when timestamp unchanged in MemoDetailSidebar
- Add same value check before updating createTime/updateTime - Skip request if new timestamp equals current timestamp - Simplify callback handlers and improve code readability - Use .some() instead of .filter().length for cleaner code
This commit is contained in:
parent
f7a81296fb
commit
97ba15450f
|
|
@ -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<CalendarSize, { gap: string; headerText: string }> = {
|
||||
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) => (
|
||||
<div className={cn("grid grid-cols-7 mb-1", GRID_STYLES[size].gap, GRID_STYLES[size].headerText)} role="row">
|
||||
{weekDays.map((label, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex h-4 items-center justify-center font-medium uppercase tracking-wide text-muted-foreground/60"
|
||||
role="columnheader"
|
||||
aria-label={label}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
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 (
|
||||
<div className={cn("flex flex-col gap-2", className)}>
|
||||
<div className={cn("grid grid-cols-7", gridGap, "text-muted-foreground mb-1", size === "small" ? "text-[10px]" : "text-xs")}>
|
||||
{rotatedWeekDays.map((label, index) => (
|
||||
<div key={index} className="flex h-4 items-center justify-center text-muted-foreground/70 font-medium uppercase tracking-wide">
|
||||
{label}
|
||||
</div>
|
||||
<div className={cn("flex flex-col", className)} role="grid" aria-label={`Calendar for ${month}`}>
|
||||
<WeekdayHeader weekDays={rotatedWeekDays} size={size} />
|
||||
|
||||
<div className={cn("grid grid-cols-7", GRID_STYLES[size].gap)} role="rowgroup">
|
||||
{flatDays.map((day) => (
|
||||
<CalendarCell
|
||||
key={day.date}
|
||||
day={day}
|
||||
maxCount={maxCount}
|
||||
tooltipText={getTooltipText(day.count, day.date, t)}
|
||||
onClick={onClick}
|
||||
size={size}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={cn("grid grid-cols-7 px-2", gridGap)}>
|
||||
{weeks.map((week, weekIndex) =>
|
||||
week.days.map((day, dayIndex) => {
|
||||
const tooltipText = getTooltipText(day.count, day.date, t);
|
||||
|
||||
return (
|
||||
<CalendarCell
|
||||
key={`${weekIndex}-${dayIndex}-${day.date}`}
|
||||
day={day}
|
||||
maxCount={maxCount}
|
||||
tooltipText={tooltipText}
|
||||
onClick={onClick}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<h2 className="text-2xl font-semibold text-foreground tracking-tight">{selectedYear}</h2>
|
||||
|
||||
<nav className="inline-flex items-center gap-0.5 rounded-lg border border-border/30 bg-muted/10 p-0.5" aria-label="Year navigation">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onPrev}
|
||||
disabled={!canGoPrev}
|
||||
aria-label="Previous year"
|
||||
className="h-7 w-7 p-0 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/40"
|
||||
>
|
||||
<ChevronLeftIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onToday}
|
||||
disabled={isCurrentYear}
|
||||
aria-label={t("common.today")}
|
||||
className={cn(
|
||||
"h-7 px-2.5 rounded-md text-[10px] font-medium uppercase tracking-wider",
|
||||
isCurrentYear ? "text-muted-foreground/50 cursor-default" : "text-muted-foreground hover:text-foreground hover:bg-muted/40",
|
||||
)}
|
||||
>
|
||||
{t("common.today")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onNext}
|
||||
disabled={!canGoNext}
|
||||
aria-label="Next year"
|
||||
className="h-7 w-7 p-0 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/40"
|
||||
>
|
||||
<ChevronRightIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
YearNavigation.displayName = "YearNavigation";
|
||||
|
||||
interface MonthCardProps {
|
||||
month: string;
|
||||
data: Record<string, number>;
|
||||
maxCount: number;
|
||||
onDateClick: (date: string) => void;
|
||||
}
|
||||
|
||||
const MonthCard = memo(({ month, data, maxCount, onDateClick }: MonthCardProps) => (
|
||||
<article className="flex flex-col gap-2 rounded-xl border border-border/20 bg-muted/5 p-3 transition-colors hover:bg-muted/10">
|
||||
<header className="text-[10px] font-medium text-muted-foreground/80 uppercase tracking-widest">{getMonthLabel(month)}</header>
|
||||
<MonthCalendar month={month} data={data} maxCount={maxCount} size="small" onClick={onDateClick} />
|
||||
</article>
|
||||
));
|
||||
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 (
|
||||
<div className={cn("w-full flex flex-col gap-6 px-4 sm:px-0 py-4 select-none", className)}>
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<div className="flex items-baseline gap-3">
|
||||
<h2 className="text-2xl md:text-3xl font-semibold text-foreground tracking-tight leading-none">{selectedYear}</h2>
|
||||
</div>
|
||||
|
||||
<div className="inline-flex items-center gap-1 shrink-0 rounded-lg border border-border/20 bg-muted/20 p-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handlePrevYear}
|
||||
disabled={!canGoPrev}
|
||||
aria-label="Previous year"
|
||||
className="h-8 w-8 p-0 rounded-md hover:bg-muted/30 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleToday}
|
||||
disabled={isCurrentYear}
|
||||
aria-label={t("common.today")}
|
||||
className={cn(
|
||||
"h-8 px-3 rounded-md text-[11px] font-semibold uppercase tracking-[0.18em] transition-colors",
|
||||
isCurrentYear ? "bg-muted/30 text-muted-foreground cursor-default" : "hover:bg-muted/30 text-foreground",
|
||||
)}
|
||||
>
|
||||
{t("common.today")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleNextYear}
|
||||
disabled={!canGoNext}
|
||||
aria-label="Next year"
|
||||
className="h-8 w-8 p-0 rounded-md hover:bg-muted/30 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ChevronRightIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<section className={cn("w-full flex flex-col gap-5 px-4 py-4 select-none", className)} aria-label={`Year ${selectedYear} calendar`}>
|
||||
<YearNavigation
|
||||
selectedYear={selectedYear}
|
||||
currentYear={currentYear}
|
||||
onPrev={() => canGoPrev && onYearChange(selectedYear - 1)}
|
||||
onNext={() => canGoNext && onYearChange(selectedYear + 1)}
|
||||
onToday={() => onYearChange(currentYear)}
|
||||
canGoPrev={canGoPrev}
|
||||
canGoNext={canGoNext}
|
||||
/>
|
||||
|
||||
<TooltipProvider>
|
||||
<div className="w-full animate-fade-in">
|
||||
<div className="grid gap-6 md:gap-7 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
{months.map((month) => (
|
||||
<div
|
||||
key={month}
|
||||
className="flex flex-col gap-3 rounded-2xl border border-border/20 bg-muted/10 p-4 shadow-sm hover:shadow-md transition-shadow cursor-default"
|
||||
>
|
||||
<div className="text-[11px] font-semibold text-muted-foreground uppercase tracking-[0.22em] pl-1">
|
||||
{getMonthLabel(month)}
|
||||
</div>
|
||||
<MonthCalendar month={month} data={yearData} maxCount={yearMaxCount} size="small" onClick={onDateClick} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 animate-fade-in">
|
||||
{months.map((month) => (
|
||||
<MonthCard key={month} month={month} data={yearData} maxCount={yearMaxCount} onDateClick={onDateClick} />
|
||||
))}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
YearCalendar.displayName = "YearCalendar";
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
>
|
||||
<div className="flex flex-col justify-start items-start w-full gap-4 h-auto shrink-0 flex-nowrap hide-scrollbar">
|
||||
{shouldShowRelationGraph && (
|
||||
{hasReferenceRelations && (
|
||||
<div className="relative w-full h-36 border border-border rounded-lg bg-muted overflow-hidden">
|
||||
<MemoRelationForceGraph className="w-full h-full" memo={memo} parentPage={parentPage} />
|
||||
<div className="absolute top-2 left-2 text-xs text-muted-foreground/60 font-medium gap-1 flex flex-row items-center">
|
||||
|
|
@ -58,16 +55,19 @@ const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<p className="text-xs font-medium text-muted-foreground/60 uppercase tracking-wide px-1">{t("common.created-at")}</p>
|
||||
<EditableTimestamp timestamp={memo.createTime} onChange={(date) => handleUpdateTimestamp("createTime", date)} />
|
||||
</div>
|
||||
|
||||
{!isEqual(memo.createTime, memo.updateTime) && (
|
||||
<div className="w-full space-y-1">
|
||||
<p className="text-xs font-medium text-muted-foreground/60 uppercase tracking-wide px-1">{t("common.last-updated-at")}</p>
|
||||
<EditableTimestamp timestamp={memo.updateTime} onChange={(date) => handleUpdateTimestamp("updateTime", date)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasSpecialProperty && (
|
||||
<div className="w-full space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground/60 uppercase tracking-wide px-1">{t("common.properties")}</p>
|
||||
|
|
@ -93,6 +93,7 @@ const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{memo.tags.length > 0 && (
|
||||
<div className="w-full space-y-2">
|
||||
<div className="flex flex-row justify-start items-center gap-1.5 px-1">
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="w-full mb-2 flex flex-row justify-between items-center gap-2">
|
||||
<header className="w-full mb-2 flex items-center justify-between gap-2">
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<button className="py-1 text-sm text-foreground font-medium transition-colors flex items-center select-none">
|
||||
{currentMonth.toLocaleString(i18n.language, { year: "numeric", month: "long" })}
|
||||
<button
|
||||
type="button"
|
||||
className="py-0.5 text-sm text-foreground font-medium transition-colors hover:text-foreground/80 select-none"
|
||||
>
|
||||
{monthLabel}
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className="p-0 border border-border/20 bg-background md:max-w-6xl w-[min(100vw-24px,1200px)] max-h-[85vh] overflow-auto rounded-2xl shadow-2xl"
|
||||
className="p-0 border border-border/20 bg-background md:max-w-6xl w-[min(100vw-24px,1200px)] max-h-[85vh] overflow-auto rounded-xl shadow-xl"
|
||||
size="2xl"
|
||||
showCloseButton={false}
|
||||
>
|
||||
|
|
@ -47,22 +58,29 @@ export const MonthNavigator = ({ visibleMonth, onMonthChange, activityStats }: M
|
|||
<YearCalendar selectedYear={currentYear} data={activityStats} onYearChange={handleYearChange} onDateClick={handleDateClick} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div className="flex justify-end items-center shrink-0">
|
||||
<button
|
||||
className="h-8 w-8 rounded-lg hover:border-border/40 hover:bg-muted/30 text-muted-foreground hover:text-foreground transition-all"
|
||||
|
||||
<nav className="flex items-center shrink-0" aria-label="Month navigation">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handlePrevMonth}
|
||||
aria-label="Previous month"
|
||||
className="h-7 w-7 p-0 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/40"
|
||||
>
|
||||
<ChevronLeftIcon className="w-4 h-4 mx-auto" />
|
||||
</button>
|
||||
<button
|
||||
className="h-8 w-8 rounded-lg hover:border-border/40 hover:bg-muted/30 text-muted-foreground hover:text-foreground transition-all"
|
||||
<ChevronLeftIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleNextMonth}
|
||||
aria-label="Next month"
|
||||
className="h-7 w-7 p-0 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/40"
|
||||
>
|
||||
<ChevronRightIcon className="w-4 h-4 mx-auto" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRightIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
MonthNavigator.displayName = "MonthNavigator";
|
||||
|
|
|
|||
Loading…
Reference in New Issue