1
+ "use client" ;
2
+
3
+ import {
4
+ eachDayOfInterval ,
5
+ endOfMonth ,
6
+ endOfWeek ,
7
+ format ,
8
+ isSameDay ,
9
+ isSameMonth ,
10
+ startOfMonth ,
11
+ startOfWeek ,
12
+ } from "date-fns" ;
13
+ import { motion } from "framer-motion" ;
14
+ import { Event , Todo , StickyNote } from "@/components/types" ;
15
+ import { priorityColors } from "@/components/priority-colors" ;
16
+
17
+ type Props = {
18
+ todos : Todo [ ] ;
19
+ events : Event [ ] ;
20
+ stickyNotes : StickyNote [ ] ;
21
+ currentMonth : Date ;
22
+ selectedDate : Date ;
23
+ handleDateSelect : ( date : Date ) => void ;
24
+ isDarkMode : boolean ;
25
+ draggedStickyNote : StickyNote | null ;
26
+ deleteStickyNote : ( id : string ) => void ;
27
+ setIsEventModalOpen : ( isOpen : boolean ) => void ;
28
+ }
29
+
30
+ function getDaysInMonth ( date : Date ) {
31
+ const start = startOfWeek ( startOfMonth ( date ) , { weekStartsOn : 0 } ) ;
32
+ const end = endOfWeek ( endOfMonth ( date ) , { weekStartsOn : 0 } ) ;
33
+ return eachDayOfInterval ( { start, end } ) ;
34
+ }
35
+
36
+ function getTodoCountForDay ( todos : Todo [ ] , day : Date ) {
37
+ return todos . filter ( ( todo ) => isSameDay ( todo . date , day ) ) . length ;
38
+ }
39
+
40
+ function getEventCountForDay ( events : Event [ ] , day : Date ) {
41
+ return events . filter ( ( event ) => event . start && isSameDay ( event . start , day ) )
42
+ . length ;
43
+ }
44
+
45
+ function getTaskIndicatorStyle ( isDarkMode : boolean , todoCount : number , eventCount : number ) {
46
+ const count = todoCount + eventCount ;
47
+ if ( count === 0 ) return "" ;
48
+ const baseColor = isDarkMode ? "bg-red-" : "bg-red-" ;
49
+ const intensity = Math . min ( count * 100 , 900 ) ;
50
+ const colorClass = `${ baseColor } ${ intensity } ` ;
51
+ return `${ colorClass } ${ count >= 3 ? "animate-pulse" : "" } ` ;
52
+ }
53
+
54
+ export function CalendarRenderer ( {
55
+ todos, events,
56
+ currentMonth, selectedDate, handleDateSelect,
57
+ isDarkMode,
58
+ draggedStickyNote, deleteStickyNote,
59
+ setIsEventModalOpen
60
+ } : Props
61
+ ) {
62
+ const days = getDaysInMonth ( currentMonth ) ;
63
+ const weeks = Math . ceil ( days . length / 7 ) ;
64
+
65
+ return (
66
+ < div className = "bg-white dark:bg-gray-800 rounded-lg shadow-md p-4" >
67
+ < div className = "grid grid-cols-7 gap-1" >
68
+ { [ "日" , "月" , "火" , "水" , "木" , "金" , "土" ] . map ( ( day ) => (
69
+ < div
70
+ key = { day }
71
+ className = "text-center font-semibold text-gray-600 dark:text-gray-300 p-2"
72
+ >
73
+ { day }
74
+ </ div >
75
+ ) ) }
76
+ </ div >
77
+ { Array . from ( { length : weeks } ) . map ( ( _ , weekIndex ) => {
78
+ const weekDays = days . slice ( weekIndex * 7 , ( weekIndex + 1 ) * 7 ) ;
79
+ const maxEventsInWeek = Math . max (
80
+ ...weekDays . map (
81
+ ( day ) => getTodoCountForDay ( todos , day ) + getEventCountForDay ( events , day ) ,
82
+ ) ,
83
+ ) ;
84
+ const weekHeight =
85
+ maxEventsInWeek > 2 ? Math . min ( maxEventsInWeek * 20 , 100 ) : "auto" ;
86
+
87
+ return (
88
+ < div
89
+ key = { weekIndex }
90
+ className = "grid grid-cols-7 gap-1"
91
+ style = { { minHeight : "100px" , height : weekHeight } }
92
+ >
93
+ { weekDays . map ( ( day ) => {
94
+ const todoCount = getTodoCountForDay ( todos , day ) ;
95
+ const eventCount = getEventCountForDay ( events , day ) ;
96
+ const isSelected = isSameDay ( day , selectedDate ) ;
97
+ const isCurrentMonth = isSameMonth ( day , currentMonth ) ;
98
+ const dayItems = [
99
+ ...todos . filter ( ( todo ) => isSameDay ( todo . date , day ) ) ,
100
+ ...events . filter (
101
+ ( event ) => event . start && isSameDay ( event . start , day ) ,
102
+ ) ,
103
+ ] ;
104
+
105
+ return (
106
+ < motion . div
107
+ key = { day . toISOString ( ) }
108
+ className = { `p-1 border rounded-md cursor-pointer transition-all duration-300 overflow-hidden ${ isSelected ? "border-blue-300 dark:border-blue-600" : "" } ${ ! isCurrentMonth
109
+ ? "text-gray-400 dark:text-gray-600 bg-gray-100 dark:bg-gray-700"
110
+ : ""
111
+ } ${ getTaskIndicatorStyle ( isDarkMode , todoCount , eventCount ) } hover:bg-gray-100 dark:hover:bg-gray-700`}
112
+ onClick = { ( ) => handleDateSelect ( day ) }
113
+ whileHover = { { scale : 1.05 } }
114
+ whileTap = { { scale : 0.95 } }
115
+ onDragOver = { ( e ) => {
116
+ e . preventDefault ( ) ;
117
+ e . currentTarget . classList . add (
118
+ "bg-blue-100" ,
119
+ "dark:bg-blue-800" ,
120
+ ) ;
121
+ } }
122
+ onDragLeave = { ( e ) => {
123
+ e . currentTarget . classList . remove (
124
+ "bg-blue-100" ,
125
+ "dark:bg-blue-800" ,
126
+ ) ;
127
+ } }
128
+ onDrop = { ( e ) => {
129
+ e . preventDefault ( ) ;
130
+ e . currentTarget . classList . remove (
131
+ "bg-blue-100" ,
132
+ "dark:bg-blue-800" ,
133
+ ) ;
134
+ if ( draggedStickyNote ) {
135
+ handleDateSelect ( day ) ;
136
+ setIsEventModalOpen ( true ) ;
137
+ deleteStickyNote ( draggedStickyNote . id ) ;
138
+ }
139
+ } }
140
+ >
141
+ < div className = "text-right text-sm" > { format ( day , "d" ) } </ div >
142
+ { ( todoCount > 0 || eventCount > 0 ) && (
143
+ < div className = "mt-1 space-y-1" >
144
+ { dayItems . slice ( 0 , 2 ) . map ( ( item , index ) => (
145
+ < div
146
+ key = { index }
147
+ className = { `text-xs p-1 rounded ${ "text" in item ? priorityColors [ item . priority ] : priorityColors [ item . priority ] } ` }
148
+ >
149
+ { "text" in item ? item . text : item . title }
150
+ </ div >
151
+ ) ) }
152
+ { dayItems . length > 2 && (
153
+ < div className = "text-xs text-center font-bold" >
154
+ +{ dayItems . length - 2 } more
155
+ </ div >
156
+ ) }
157
+ </ div >
158
+ ) }
159
+ </ motion . div >
160
+ ) ;
161
+ } ) }
162
+ </ div >
163
+ ) ;
164
+ } ) }
165
+ </ div >
166
+ ) ;
167
+ } ;
0 commit comments