Skip to content

Commit b826e90

Browse files
committed
refactor: polish ActivityCalendar components with modern design
1 parent 5d67782 commit b826e90

File tree

17 files changed

+193
-329
lines changed

17 files changed

+193
-329
lines changed

web/src/components/ActivityCalendar/ActivityCalendar.tsx

Lines changed: 0 additions & 63 deletions
This file was deleted.

web/src/components/ActivityCalendar/CalendarCell.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const CalendarCell = memo((props: CalendarCellProps) => {
2626
const smallExtraClasses = size === "small" ? `${SMALL_CELL_SIZE.dimensions} min-h-0` : "";
2727

2828
const baseClasses = cn(
29-
"aspect-square w-full border 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",
29+
"aspect-square w-full flex items-center justify-center text-center transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-2 select-none",
3030
sizeConfig.font,
3131
sizeConfig.borderRadius,
3232
smallExtraClasses,
@@ -35,23 +35,17 @@ export const CalendarCell = memo((props: CalendarCellProps) => {
3535
const ariaLabel = day.isSelected ? `${tooltipText} (selected)` : tooltipText;
3636

3737
if (!day.isCurrentMonth) {
38-
return (
39-
<div className={cn(baseClasses, "border-transparent text-muted-foreground/60 bg-transparent pointer-events-none opacity-80")}>
40-
{day.label}
41-
</div>
42-
);
38+
return <div className={cn(baseClasses, "text-muted-foreground/30 bg-transparent cursor-default")}>{day.label}</div>;
4339
}
4440

4541
const intensityClass = getCellIntensityClass(day, maxCount);
4642

4743
const buttonClasses = cn(
4844
baseClasses,
49-
"border-transparent text-muted-foreground",
50-
(day.isToday || day.isSelected) && "border-border",
51-
day.isSelected && "font-medium",
52-
day.isWeekend && "text-muted-foreground/80",
5345
intensityClass,
54-
isInteractive ? "cursor-pointer hover:scale-105" : "cursor-default",
46+
day.isToday && "ring-2 ring-primary/30 ring-offset-1 font-semibold z-10",
47+
day.isSelected && "ring-2 ring-primary ring-offset-1 font-bold z-10",
48+
isInteractive ? "cursor-pointer hover:scale-110 hover:shadow-md hover:z-20" : "cursor-default",
5549
);
5650

5751
const button = (

web/src/components/ActivityCalendar/CalendarHeader.tsx

Lines changed: 0 additions & 73 deletions
This file was deleted.

web/src/components/ActivityCalendar/CalendarPopover.tsx

Lines changed: 0 additions & 32 deletions
This file was deleted.

web/src/components/ActivityCalendar/CompactMonthCalendar.tsx renamed to web/src/components/ActivityCalendar/MonthCalendar.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import { cn } from "@/lib/utils";
44
import { useTranslate } from "@/utils/i18n";
55
import { CalendarCell } from "./CalendarCell";
66
import { DEFAULT_CELL_SIZE, SMALL_CELL_SIZE } from "./constants";
7-
import { getTooltipText, useTodayDate, useWeekdayLabels } from "./shared";
8-
import type { CompactMonthCalendarProps } from "./types";
9-
import { useCalendarMatrix } from "./useCalendarMatrix";
7+
import { useTodayDate, useWeekdayLabels } from "./hooks";
8+
import type { MonthCalendarProps } from "./types";
9+
import { useCalendarMatrix } from "./useCalendar";
10+
import { getTooltipText } from "./utils";
1011

11-
export const CompactMonthCalendar = memo((props: CompactMonthCalendarProps) => {
12-
const { month, data, maxCount, size = "default", onClick } = props;
12+
export const MonthCalendar = memo((props: MonthCalendarProps) => {
13+
const { month, data, maxCount, size = "default", onClick, className } = props;
1314
const t = useTranslate();
1415
const { generalSetting } = useInstance();
1516

@@ -30,10 +31,10 @@ export const CompactMonthCalendar = memo((props: CompactMonthCalendarProps) => {
3031
const sizeConfig = size === "small" ? SMALL_CELL_SIZE : DEFAULT_CELL_SIZE;
3132

3233
return (
33-
<div className="flex flex-col gap-1">
34-
<div className={cn("grid grid-cols-7 gap-0.5 text-muted-foreground", size === "small" ? "text-[10px]" : "text-xs")}>
34+
<div className={cn("flex flex-col gap-2", className)}>
35+
<div className={cn("grid grid-cols-7", sizeConfig.gap, "text-muted-foreground mb-1", size === "small" ? "text-[10px]" : "text-xs")}>
3536
{rotatedWeekDays.map((label, index) => (
36-
<div key={index} className="flex h-4 items-center justify-center text-muted-foreground/50">
37+
<div key={index} className="flex h-4 items-center justify-center text-muted-foreground/60 font-medium">
3738
{label}
3839
</div>
3940
))}
@@ -61,4 +62,4 @@ export const CompactMonthCalendar = memo((props: CompactMonthCalendarProps) => {
6162
);
6263
});
6364

64-
CompactMonthCalendar.displayName = "CompactMonthCalendar";
65+
MonthCalendar.displayName = "MonthCalendar";

web/src/components/ActivityCalendar/MonthCard.tsx

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
2+
import { useMemo } from "react";
3+
import {
4+
calculateYearMaxCount,
5+
filterDataByYear,
6+
generateMonthsForYear,
7+
getMonthLabel,
8+
MonthCalendar,
9+
} from "@/components/ActivityCalendar";
10+
import { Button } from "@/components/ui/button";
11+
import { TooltipProvider } from "@/components/ui/tooltip";
12+
import { cn } from "@/lib/utils";
13+
import { useTranslate } from "@/utils/i18n";
14+
import { getMaxYear, MIN_YEAR } from "./constants";
15+
import type { YearCalendarProps } from "./types";
16+
17+
export const YearCalendar = ({ selectedYear, data, onYearChange, onDateClick, className }: YearCalendarProps) => {
18+
const t = useTranslate();
19+
const currentYear = useMemo(() => new Date().getFullYear(), []);
20+
const yearData = useMemo(() => filterDataByYear(data, selectedYear), [data, selectedYear]);
21+
const months = useMemo(() => generateMonthsForYear(selectedYear), [selectedYear]);
22+
const yearMaxCount = useMemo(() => calculateYearMaxCount(yearData), [yearData]);
23+
24+
const canGoPrev = selectedYear > MIN_YEAR;
25+
const canGoNext = selectedYear < getMaxYear();
26+
const isCurrentYear = selectedYear === currentYear;
27+
28+
const handlePrevYear = () => canGoPrev && onYearChange(selectedYear - 1);
29+
const handleNextYear = () => canGoNext && onYearChange(selectedYear + 1);
30+
const handleToday = () => onYearChange(currentYear);
31+
32+
return (
33+
<div className={cn("w-full flex flex-col gap-6 p-2 md:p-0 select-none", className)}>
34+
<div className="flex items-center justify-between pb-4 px-2 pt-2">
35+
<h2 className="text-3xl font-bold text-foreground tracking-tight leading-none">{selectedYear}</h2>
36+
37+
<div className="inline-flex items-center gap-1 shrink-0">
38+
<Button
39+
variant="ghost"
40+
size="sm"
41+
onClick={handlePrevYear}
42+
disabled={!canGoPrev}
43+
aria-label="Previous year"
44+
className="h-9 w-9 p-0 rounded-md hover:bg-secondary/80 text-muted-foreground hover:text-foreground"
45+
>
46+
<ChevronLeftIcon className="w-5 h-5" />
47+
</Button>
48+
49+
<Button
50+
variant="ghost"
51+
size="sm"
52+
onClick={handleToday}
53+
disabled={isCurrentYear}
54+
aria-label={t("common.today")}
55+
className={cn(
56+
"h-9 px-4 rounded-md text-sm font-medium transition-colors",
57+
isCurrentYear ? "bg-secondary/50 text-muted-foreground cursor-default" : "hover:bg-secondary/80 text-foreground",
58+
)}
59+
>
60+
{t("common.today")}
61+
</Button>
62+
63+
<Button
64+
variant="ghost"
65+
size="sm"
66+
onClick={handleNextYear}
67+
disabled={!canGoNext}
68+
aria-label="Next year"
69+
className="h-9 w-9 p-0 rounded-md hover:bg-secondary/80 text-muted-foreground hover:text-foreground"
70+
>
71+
<ChevronRightIcon className="w-5 h-5" />
72+
</Button>
73+
</div>
74+
</div>
75+
76+
<TooltipProvider>
77+
<div className="w-full animate-fade-in">
78+
<div className="grid gap-6 grid-cols-1 sm:grid-cols-2 md:grid-cols-3">
79+
{months.map((month) => (
80+
<div
81+
key={month}
82+
className="flex flex-col gap-3 rounded-lg p-3 hover:bg-secondary/40 transition-colors cursor-default border border-transparent hover:border-border/50"
83+
>
84+
<div className="text-xs font-bold text-foreground/80 uppercase tracking-widest pl-1">{getMonthLabel(month)}</div>
85+
<MonthCalendar month={month} data={yearData} maxCount={yearMaxCount} size="small" onClick={onDateClick} />
86+
</div>
87+
))}
88+
</div>
89+
</div>
90+
</TooltipProvider>
91+
</div>
92+
);
93+
};

web/src/components/ActivityCalendar/constants.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,23 @@ export const INTENSITY_THRESHOLDS = {
1313
MINIMAL: 0,
1414
} as const;
1515

16+
export const CELL_STYLES = {
17+
HIGH: "bg-primary text-primary-foreground shadow-sm",
18+
MEDIUM: "bg-primary/80 text-primary-foreground shadow-sm",
19+
LOW: "bg-primary/60 text-primary-foreground shadow-sm",
20+
MINIMAL: "bg-primary/40 text-foreground",
21+
EMPTY: "bg-secondary/30 text-muted-foreground hover:bg-secondary/50",
22+
} as const;
23+
1624
export const SMALL_CELL_SIZE = {
17-
font: "text-[10px]",
18-
dimensions: "max-w-6 max-h-6",
19-
borderRadius: "rounded-sm",
20-
gap: "gap-px",
25+
font: "text-xs",
26+
dimensions: "w-8 h-8 mx-auto",
27+
borderRadius: "rounded-md",
28+
gap: "gap-1",
2129
} as const;
2230

2331
export const DEFAULT_CELL_SIZE = {
2432
font: "text-xs",
25-
borderRadius: "rounded",
26-
gap: "gap-0.5",
33+
borderRadius: "rounded-md",
34+
gap: "gap-1.5",
2735
} as const;

0 commit comments

Comments
 (0)