@tuturuuu/ui 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/.checksum +1 -0
  2. package/README.md +46 -0
  3. package/components.json +20 -0
  4. package/eslint.config.mjs +20 -0
  5. package/jsr.json +10 -0
  6. package/package.json +120 -0
  7. package/postcss.config.mjs +8 -0
  8. package/rollup.config.js +40 -0
  9. package/src/components/ui/accordion.tsx +70 -0
  10. package/src/components/ui/alert-dialog.tsx +156 -0
  11. package/src/components/ui/alert.tsx +58 -0
  12. package/src/components/ui/aspect-ratio.tsx +11 -0
  13. package/src/components/ui/avatar.tsx +52 -0
  14. package/src/components/ui/badge.tsx +49 -0
  15. package/src/components/ui/breadcrumb.tsx +108 -0
  16. package/src/components/ui/button.tsx +61 -0
  17. package/src/components/ui/calendar.tsx +212 -0
  18. package/src/components/ui/card.tsx +74 -0
  19. package/src/components/ui/carousel.tsx +240 -0
  20. package/src/components/ui/chart.tsx +365 -0
  21. package/src/components/ui/checkbox.tsx +31 -0
  22. package/src/components/ui/codeblock.tsx +161 -0
  23. package/src/components/ui/collapsible.tsx +33 -0
  24. package/src/components/ui/color-picker.tsx +143 -0
  25. package/src/components/ui/command.tsx +176 -0
  26. package/src/components/ui/context-menu.tsx +251 -0
  27. package/src/components/ui/custom/autosize-textarea.tsx +111 -0
  28. package/src/components/ui/custom/calendar/core.tsx +61 -0
  29. package/src/components/ui/custom/calendar/day-cell.tsx +74 -0
  30. package/src/components/ui/custom/calendar/month-header.tsx +59 -0
  31. package/src/components/ui/custom/calendar/month-view.tsx +110 -0
  32. package/src/components/ui/custom/calendar/utils.ts +76 -0
  33. package/src/components/ui/custom/calendar/year-calendar.tsx +64 -0
  34. package/src/components/ui/custom/calendar/year-view.tsx +58 -0
  35. package/src/components/ui/custom/combobox.tsx +197 -0
  36. package/src/components/ui/custom/common-footer.tsx +215 -0
  37. package/src/components/ui/custom/compared-date-range-picker.tsx +561 -0
  38. package/src/components/ui/custom/date-input.tsx +279 -0
  39. package/src/components/ui/custom/empty-card.tsx +39 -0
  40. package/src/components/ui/custom/feature-summary.tsx +135 -0
  41. package/src/components/ui/custom/file-uploader.tsx +349 -0
  42. package/src/components/ui/custom/input-field.tsx +29 -0
  43. package/src/components/ui/custom/loading-indicator.tsx +28 -0
  44. package/src/components/ui/custom/modifiable-dialog-trigger.tsx +83 -0
  45. package/src/components/ui/custom/month-picker.tsx +157 -0
  46. package/src/components/ui/custom/report-preview.tsx +175 -0
  47. package/src/components/ui/custom/search-bar.tsx +56 -0
  48. package/src/components/ui/custom/select-field.tsx +78 -0
  49. package/src/components/ui/custom/tables/data-table-column-header.tsx +72 -0
  50. package/src/components/ui/custom/tables/data-table-create-button.tsx +31 -0
  51. package/src/components/ui/custom/tables/data-table-faceted-filter.tsx +142 -0
  52. package/src/components/ui/custom/tables/data-table-pagination.tsx +243 -0
  53. package/src/components/ui/custom/tables/data-table-refresh-button.tsx +45 -0
  54. package/src/components/ui/custom/tables/data-table-toolbar.tsx +133 -0
  55. package/src/components/ui/custom/tables/data-table-view-options.tsx +112 -0
  56. package/src/components/ui/custom/tables/data-table.tsx +228 -0
  57. package/src/components/ui/custom/uploaded-files-card.tsx +50 -0
  58. package/src/components/ui/dialog.tsx +137 -0
  59. package/src/components/ui/drawer.tsx +131 -0
  60. package/src/components/ui/dropdown-menu.tsx +256 -0
  61. package/src/components/ui/form.tsx +167 -0
  62. package/src/components/ui/hover-card.tsx +41 -0
  63. package/src/components/ui/icons.tsx +506 -0
  64. package/src/components/ui/input-otp.tsx +78 -0
  65. package/src/components/ui/input.tsx +18 -0
  66. package/src/components/ui/label.tsx +23 -0
  67. package/src/components/ui/markdown.tsx +7 -0
  68. package/src/components/ui/menubar.tsx +275 -0
  69. package/src/components/ui/navigation-menu.tsx +169 -0
  70. package/src/components/ui/pagination.tsx +126 -0
  71. package/src/components/ui/popover.tsx +47 -0
  72. package/src/components/ui/progress.tsx +30 -0
  73. package/src/components/ui/radio-group.tsx +44 -0
  74. package/src/components/ui/resizable.tsx +55 -0
  75. package/src/components/ui/scroll-area.tsx +57 -0
  76. package/src/components/ui/select.tsx +180 -0
  77. package/src/components/ui/separator.tsx +27 -0
  78. package/src/components/ui/sheet.tsx +138 -0
  79. package/src/components/ui/sidebar.tsx +734 -0
  80. package/src/components/ui/skeleton.tsx +13 -0
  81. package/src/components/ui/slider.tsx +62 -0
  82. package/src/components/ui/sonner.tsx +29 -0
  83. package/src/components/ui/switch.tsx +30 -0
  84. package/src/components/ui/table.tsx +112 -0
  85. package/src/components/ui/tabs.tsx +68 -0
  86. package/src/components/ui/tag-input.tsx +141 -0
  87. package/src/components/ui/textarea.tsx +17 -0
  88. package/src/components/ui/time-picker-input.tsx +117 -0
  89. package/src/components/ui/time-picker-utils.tsx +146 -0
  90. package/src/components/ui/toast.tsx +128 -0
  91. package/src/components/ui/toaster.tsx +35 -0
  92. package/src/components/ui/toggle-group.tsx +72 -0
  93. package/src/components/ui/toggle.tsx +46 -0
  94. package/src/components/ui/tooltip.tsx +60 -0
  95. package/src/globals.css +252 -0
  96. package/src/hooks/use-callback-ref.ts +28 -0
  97. package/src/hooks/use-controllable-state.ts +68 -0
  98. package/src/hooks/use-copy-to-clipboard.ts +46 -0
  99. package/src/hooks/use-form.ts +23 -0
  100. package/src/hooks/use-forwarded-ref.ts +17 -0
  101. package/src/hooks/use-mobile.tsx +21 -0
  102. package/src/hooks/use-toast.ts +191 -0
  103. package/src/resolvers.ts +3 -0
  104. package/tsconfig.json +17 -0
@@ -0,0 +1,61 @@
1
+ 'use client';
2
+
3
+ import { MonthView } from './month-view';
4
+ import { WorkspaceUserAttendance } from './utils';
5
+ import { YearView } from './year-view';
6
+ import { useEffect, useState } from 'react';
7
+
8
+ interface CalendarProps {
9
+ locale: string;
10
+ initialDate?: Date;
11
+ attendanceData?: WorkspaceUserAttendance[];
12
+ // eslint-disable-next-line no-unused-vars
13
+ onDateClick?: (date: Date) => void;
14
+ hideControls?: boolean;
15
+ hideYear?: boolean;
16
+ }
17
+
18
+ export const Calendar: React.FC<CalendarProps> = ({
19
+ locale,
20
+ initialDate,
21
+ attendanceData,
22
+ onDateClick,
23
+ hideControls = false,
24
+ hideYear = false,
25
+ }) => {
26
+ const [currentDate, setCurrentDate] = useState(initialDate || new Date());
27
+ const [viewMode, setViewMode] = useState<'month' | 'year'>('month');
28
+
29
+ useEffect(() => {
30
+ setCurrentDate(initialDate || new Date());
31
+ }, [initialDate]);
32
+
33
+ const handleMonthClick = (month: number) => {
34
+ setCurrentDate(new Date(currentDate.setMonth(month)));
35
+ setViewMode('month');
36
+ };
37
+
38
+ const handleYearViewClick = () => {
39
+ setViewMode('year');
40
+ };
41
+
42
+ return viewMode === 'month' ? (
43
+ <MonthView
44
+ locale={locale}
45
+ currentDate={currentDate}
46
+ setCurrentDate={setCurrentDate}
47
+ attendanceData={attendanceData}
48
+ onDateClick={onDateClick}
49
+ onYearViewClick={handleYearViewClick}
50
+ hideControls={hideControls}
51
+ hideYear={hideYear}
52
+ />
53
+ ) : (
54
+ <YearView
55
+ locale={locale}
56
+ currentDate={currentDate}
57
+ setCurrentDate={setCurrentDate}
58
+ handleMonthClick={handleMonthClick}
59
+ />
60
+ );
61
+ };
@@ -0,0 +1,74 @@
1
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../../tooltip';
2
+ import {
3
+ WorkspaceUserAttendance,
4
+ getAttendanceGroupNames,
5
+ isCurrentMonth,
6
+ isDateAbsent,
7
+ isDateAttended,
8
+ } from './utils';
9
+ import { cn } from '@tuturuuu/utils/format';
10
+ import { isAfter } from 'date-fns';
11
+ import { Fragment } from 'react';
12
+
13
+ export const DayCell: React.FC<{
14
+ day: Date;
15
+ currentDate: Date;
16
+ today: Date;
17
+ attendanceData?: WorkspaceUserAttendance[];
18
+ // eslint-disable-next-line no-unused-vars
19
+ onDateClick?: (date: Date) => void;
20
+ }> = ({ day, currentDate, today, attendanceData, onDateClick }) => {
21
+ if (!isCurrentMonth(day, currentDate))
22
+ return (
23
+ <div className="flex flex-none cursor-default justify-center rounded border border-transparent p-2 font-semibold text-foreground/20 transition duration-300 md:rounded-lg">
24
+ {day.getDate()}
25
+ </div>
26
+ );
27
+
28
+ if (
29
+ !isDateAttended(day, attendanceData) &&
30
+ !isDateAbsent(day, attendanceData)
31
+ )
32
+ return (
33
+ <button
34
+ onClick={onDateClick ? () => onDateClick(day) : undefined}
35
+ className={cn(
36
+ 'flex flex-none cursor-default justify-center rounded border bg-foreground/5 p-2 font-semibold text-foreground/40 transition duration-300 hover:cursor-pointer md:rounded-lg dark:bg-foreground/10',
37
+ isAfter(day, today) &&
38
+ 'cursor-not-allowed opacity-50 hover:cursor-not-allowed'
39
+ )}
40
+ >
41
+ {day.getDate()}
42
+ </button>
43
+ );
44
+
45
+ return (
46
+ <Fragment>
47
+ <Tooltip>
48
+ <TooltipTrigger asChild>
49
+ <button
50
+ onClick={onDateClick ? () => onDateClick(day) : undefined}
51
+ className={`flex flex-none cursor-pointer justify-center rounded border p-2 font-semibold transition duration-300 md:rounded-lg ${
52
+ isDateAttended(day, attendanceData)
53
+ ? 'border-green-500/30 bg-green-500/10 text-green-600 dark:border-green-300/20 dark:bg-green-300/20 dark:text-green-300'
54
+ : isDateAbsent(day, attendanceData)
55
+ ? 'border-red-500/30 bg-red-500/10 text-red-600 dark:border-red-300/20 dark:bg-red-300/20 dark:text-red-300'
56
+ : 'bg-foreground/5 text-foreground/40 dark:bg-foreground/10'
57
+ }`}
58
+ >
59
+ {day.getDate()}
60
+ </button>
61
+ </TooltipTrigger>
62
+ <TooltipContent>
63
+ {getAttendanceGroupNames(day, attendanceData).map(
64
+ (groupName, idx) => (
65
+ <div key={groupName + idx} className="flex items-center gap-1">
66
+ <span className="text-xs font-semibold">{groupName}</span>
67
+ </div>
68
+ )
69
+ )}
70
+ </TooltipContent>
71
+ </Tooltip>
72
+ </Fragment>
73
+ );
74
+ };
@@ -0,0 +1,59 @@
1
+ import { Button } from '../../button';
2
+ import { cn } from '@tuturuuu/utils/format';
3
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
4
+
5
+ export const MonthHeader: React.FC<{
6
+ thisYear: number;
7
+ thisMonth: string;
8
+ handlePrev: () => void;
9
+ handleNext: () => void;
10
+ currentDate: Date;
11
+ onYearViewClick: () => void;
12
+ hideControls?: boolean;
13
+ hideYear?: boolean;
14
+ }> = ({
15
+ thisYear,
16
+ thisMonth,
17
+ handlePrev,
18
+ handleNext,
19
+ currentDate,
20
+ onYearViewClick,
21
+ hideControls = false,
22
+ hideYear = false,
23
+ }) => (
24
+ <div
25
+ className={cn(
26
+ 'flex flex-wrap items-center justify-between gap-x-4 gap-y-1 text-xl font-bold md:text-2xl',
27
+ hideControls || 'mb-4'
28
+ )}
29
+ >
30
+ <div className="flex items-center gap-1">
31
+ {hideYear || thisYear}
32
+ {hideYear || (
33
+ <div className="mx-2 h-4 w-[1px] rotate-[30deg] bg-foreground/20" />
34
+ )}
35
+ <span className="text-lg font-semibold md:text-xl">{thisMonth}</span>
36
+ </div>
37
+ {!hideControls && (
38
+ <div className="flex items-center gap-1">
39
+ <Button size="xs" variant="secondary" onClick={onYearViewClick}>
40
+ Year View
41
+ </Button>
42
+ <Button size="xs" variant="secondary" onClick={handlePrev}>
43
+ <ChevronLeft className="h-6 w-6" />
44
+ </Button>
45
+ <Button
46
+ size="xs"
47
+ variant="secondary"
48
+ onClick={handleNext}
49
+ disabled={
50
+ currentDate.getMonth() === new Date().getMonth() &&
51
+ currentDate.getFullYear() === new Date().getFullYear()
52
+ }
53
+ >
54
+ <ChevronRight className="h-6 w-6" />
55
+ </Button>
56
+ </div>
57
+ )}
58
+ </div>
59
+ );
@@ -0,0 +1,110 @@
1
+ import { Separator } from '../../separator';
2
+ import { TooltipProvider } from '../../tooltip';
3
+ import { DayCell } from './day-cell';
4
+ import { MonthHeader } from './month-header';
5
+ import { WorkspaceUserAttendance } from './utils';
6
+ import { startOfDay } from 'date-fns';
7
+ import { useMemo } from 'react';
8
+
9
+ export const MonthView: React.FC<{
10
+ locale: string;
11
+ currentDate: Date;
12
+ setCurrentDate: React.Dispatch<React.SetStateAction<Date>>;
13
+ attendanceData?: WorkspaceUserAttendance[];
14
+ // eslint-disable-next-line no-unused-vars
15
+ onDateClick?: (date: Date) => void;
16
+ onYearViewClick: () => void;
17
+ hideControls?: boolean;
18
+ hideYear?: boolean;
19
+ }> = ({
20
+ locale,
21
+ currentDate,
22
+ setCurrentDate,
23
+ attendanceData,
24
+ onDateClick,
25
+ onYearViewClick,
26
+ hideControls = false,
27
+ hideYear = false,
28
+ }) => {
29
+ const thisYear = currentDate.getFullYear();
30
+ const thisMonth = currentDate.toLocaleString(locale, {
31
+ month: hideYear ? 'long' : '2-digit',
32
+ });
33
+ const today = startOfDay(new Date());
34
+
35
+ const days = useMemo(
36
+ () =>
37
+ Array.from({ length: 7 }, (_, i) => {
38
+ let newDay = new Date(currentDate);
39
+ newDay.setDate(currentDate.getDate() - currentDate.getDay() + i + 1);
40
+ return newDay.toLocaleString(locale, { weekday: 'narrow' });
41
+ }),
42
+ [currentDate, locale]
43
+ );
44
+
45
+ const daysInMonth = useMemo(
46
+ () =>
47
+ Array.from({ length: 42 }, (_, i) => {
48
+ let newDay = new Date(
49
+ currentDate.getFullYear(),
50
+ currentDate.getMonth(),
51
+ 1
52
+ );
53
+ let dayOfWeek = newDay.getDay();
54
+ let adjustment = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
55
+ newDay.setDate(newDay.getDate() - adjustment + i);
56
+ return newDay;
57
+ }),
58
+ [currentDate]
59
+ );
60
+
61
+ const handlePrev = () =>
62
+ setCurrentDate(new Date(currentDate.setMonth(currentDate.getMonth() - 1)));
63
+ const handleNext = () =>
64
+ setCurrentDate(new Date(currentDate.setMonth(currentDate.getMonth() + 1)));
65
+
66
+ return (
67
+ <div>
68
+ <MonthHeader
69
+ thisYear={thisYear}
70
+ thisMonth={thisMonth}
71
+ handlePrev={handlePrev}
72
+ handleNext={handleNext}
73
+ currentDate={currentDate}
74
+ onYearViewClick={onYearViewClick}
75
+ hideControls={hideControls}
76
+ hideYear={hideYear}
77
+ />
78
+
79
+ {hideControls && <Separator className="my-2" />}
80
+
81
+ <div className="relative grid gap-1 text-xs md:gap-2 md:text-base">
82
+ <div className="grid grid-cols-7 gap-1 md:gap-2">
83
+ {days.map((day, idx) => (
84
+ <div
85
+ key={`day-${idx}`}
86
+ className="flex flex-none cursor-default justify-center rounded bg-foreground/5 p-2 font-semibold transition duration-300 md:rounded-lg"
87
+ >
88
+ {day}
89
+ </div>
90
+ ))}
91
+ </div>
92
+
93
+ <div className="grid grid-cols-7 gap-1 md:gap-2">
94
+ <TooltipProvider delayDuration={0}>
95
+ {daysInMonth.map((day, idx) => (
96
+ <DayCell
97
+ key={`day-${idx}`}
98
+ day={day}
99
+ currentDate={currentDate}
100
+ today={today}
101
+ attendanceData={attendanceData}
102
+ onDateClick={onDateClick}
103
+ />
104
+ ))}
105
+ </TooltipProvider>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ );
110
+ };
@@ -0,0 +1,76 @@
1
+ export interface WorkspaceUserAttendance {
2
+ date: string;
3
+ status: 'PRESENT' | 'ABSENT';
4
+ groups?: {
5
+ id: string;
6
+ name: string;
7
+ }[];
8
+ }
9
+
10
+ export const isCurrentMonth = (date: Date, currentDate: Date) =>
11
+ date.getMonth() === currentDate.getMonth() &&
12
+ date.getFullYear() === currentDate.getFullYear();
13
+
14
+ export const isDateAttended = (
15
+ date: Date,
16
+ attendanceData?: WorkspaceUserAttendance[]
17
+ ) =>
18
+ attendanceData
19
+ ? attendanceData.some((attendance) => {
20
+ const attendanceDate = new Date(attendance.date);
21
+ return (
22
+ attendanceDate.getDate() === date.getDate() &&
23
+ attendanceDate.getMonth() === date.getMonth() &&
24
+ attendanceDate.getFullYear() === date.getFullYear() &&
25
+ attendance.status === 'PRESENT'
26
+ );
27
+ })
28
+ : false;
29
+
30
+ export const isDateAbsent = (
31
+ date: Date,
32
+ attendanceData?: WorkspaceUserAttendance[]
33
+ ) =>
34
+ attendanceData
35
+ ? attendanceData.some((attendance) => {
36
+ const attendanceDate = new Date(attendance.date);
37
+ return (
38
+ attendanceDate.getDate() === date.getDate() &&
39
+ attendanceDate.getMonth() === date.getMonth() &&
40
+ attendanceDate.getFullYear() === date.getFullYear() &&
41
+ attendance.status === 'ABSENT'
42
+ );
43
+ })
44
+ : false;
45
+
46
+ export const getAttendanceGroupNames = (
47
+ date: Date,
48
+ attendanceData?: WorkspaceUserAttendance[]
49
+ ): string[] => {
50
+ if (!attendanceData) return [];
51
+ const filteredAttendance = attendanceData.filter((attendance) => {
52
+ const attendanceDate = new Date(attendance.date);
53
+ return (
54
+ attendanceDate.getDate() === date.getDate() &&
55
+ attendanceDate.getMonth() === date.getMonth() &&
56
+ attendanceDate.getFullYear() === date.getFullYear()
57
+ );
58
+ });
59
+
60
+ const uniqueGroups = filteredAttendance.reduce(
61
+ (acc, curr) => {
62
+ Array.isArray(curr.groups)
63
+ ? curr.groups.forEach((group) => {
64
+ if (!acc.some((g) => g.id === group.id)) {
65
+ acc.push(group);
66
+ }
67
+ })
68
+ : curr.groups && acc.push(curr.groups);
69
+
70
+ return acc;
71
+ },
72
+ [] as { id: string; name: string }[]
73
+ );
74
+
75
+ return uniqueGroups.map((group) => group.name);
76
+ };
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+
3
+ import { Calendar } from './core';
4
+ import { WorkspaceUserAttendance } from './utils';
5
+ import { Button } from '@tuturuuu/ui/button';
6
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
7
+ import { useState } from 'react';
8
+
9
+ interface YearCalendarProps {
10
+ locale: string;
11
+ initialDate?: Date;
12
+ attendanceData?: WorkspaceUserAttendance[];
13
+ // eslint-disable-next-line no-unused-vars
14
+ onDateClick?: (date: Date) => void;
15
+ }
16
+
17
+ export const YearCalendar: React.FC<YearCalendarProps> = ({
18
+ locale,
19
+ initialDate,
20
+ attendanceData,
21
+ onDateClick,
22
+ }) => {
23
+ const [currentYear, setCurrentYear] = useState(
24
+ initialDate?.getFullYear() || new Date().getFullYear()
25
+ );
26
+
27
+ const handlePrevYear = () => setCurrentYear(currentYear - 1);
28
+ const handleNextYear = () => setCurrentYear(currentYear + 1);
29
+
30
+ const months = Array.from(
31
+ { length: 12 },
32
+ (_, i) => new Date(currentYear, i, 1)
33
+ );
34
+
35
+ return (
36
+ <div>
37
+ <div className="mb-4 flex flex-wrap items-center justify-between gap-x-4 gap-y-1 rounded-lg border bg-foreground/5 p-4 text-xl font-bold md:text-2xl">
38
+ <div className="flex items-center gap-1">{currentYear}</div>
39
+ <div className="flex items-center gap-1">
40
+ <Button size="xs" variant="secondary" onClick={handlePrevYear}>
41
+ <ChevronLeft className="h-6 w-6" />
42
+ </Button>
43
+ <Button size="xs" variant="secondary" onClick={handleNextYear}>
44
+ <ChevronRight className="h-6 w-6" />
45
+ </Button>
46
+ </div>
47
+ </div>
48
+
49
+ <div className="grid gap-4 lg:grid-cols-2 xl:grid-cols-3">
50
+ {months.map((month, idx) => (
51
+ <div key={idx} className="rounded-lg border p-2">
52
+ <Calendar
53
+ locale={locale}
54
+ initialDate={month}
55
+ attendanceData={attendanceData}
56
+ onDateClick={onDateClick}
57
+ hideControls
58
+ />
59
+ </div>
60
+ ))}
61
+ </div>
62
+ </div>
63
+ );
64
+ };
@@ -0,0 +1,58 @@
1
+ import { Button } from '../../button';
2
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
3
+ import { useMemo } from 'react';
4
+
5
+ export const YearView: React.FC<{
6
+ locale: string;
7
+ currentDate: Date;
8
+ setCurrentDate: React.Dispatch<React.SetStateAction<Date>>;
9
+ // eslint-disable-next-line no-unused-vars
10
+ handleMonthClick: (month: number) => void;
11
+ }> = ({ locale, currentDate, setCurrentDate, handleMonthClick }) => {
12
+ const thisYear = currentDate.getFullYear();
13
+ const months = useMemo(
14
+ () =>
15
+ Array.from(
16
+ { length: 12 },
17
+ (_, i) => new Date(currentDate.getFullYear(), i, 1)
18
+ ),
19
+ [currentDate]
20
+ );
21
+
22
+ const handlePrev = () =>
23
+ setCurrentDate(
24
+ new Date(currentDate.setFullYear(currentDate.getFullYear() - 1))
25
+ );
26
+ const handleNext = () =>
27
+ setCurrentDate(
28
+ new Date(currentDate.setFullYear(currentDate.getFullYear() + 1))
29
+ );
30
+
31
+ return (
32
+ <div>
33
+ <div className="mb-4 flex flex-wrap items-center justify-between gap-x-4 gap-y-1 text-xl font-bold md:text-2xl">
34
+ <div className="flex items-center gap-1">{thisYear}</div>
35
+ <div className="flex items-center gap-1">
36
+ <Button size="xs" variant="secondary" onClick={handlePrev}>
37
+ <ChevronLeft className="h-6 w-6" />
38
+ </Button>
39
+ <Button size="xs" variant="secondary" onClick={handleNext}>
40
+ <ChevronRight className="h-6 w-6" />
41
+ </Button>
42
+ </div>
43
+ </div>
44
+
45
+ <div className="grid grid-cols-3 gap-4">
46
+ {months.map((month, idx) => (
47
+ <button
48
+ key={`month-${idx}`}
49
+ onClick={() => handleMonthClick(month.getMonth())}
50
+ className="flex flex-none cursor-pointer justify-center rounded bg-foreground/5 p-4 font-semibold transition duration-300 hover:bg-foreground/10"
51
+ >
52
+ {month.toLocaleString(locale, { month: 'long' })}
53
+ </button>
54
+ ))}
55
+ </div>
56
+ </div>
57
+ );
58
+ };