Skip to content

Commit 8e28f72

Browse files
committed
commit chnages on calendar control
1 parent 1e218d3 commit 8e28f72

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+3892
-689
lines changed

package-lock.json

Lines changed: 1127 additions & 676 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@fluentui/styles": "0.66.5",
3737
"@fluentui/theme": "^2.6.6",
3838
"@iconify/react": "^4.1.1",
39+
"@juggle/resize-observer": "^3.4.0",
3940
"@microsoft/decorators": "1.20.0",
4041
"@microsoft/mgt-react": "3.1.3",
4142
"@microsoft/mgt-spfx": "3.1.3",
@@ -54,6 +55,7 @@
5455
"@microsoft/sp-property-pane": "1.20.0",
5556
"@microsoft/sp-webpart-base": "1.20.0",
5657
"@monaco-editor/loader": "^1.3.1",
58+
"@nuvemerudita/react-controls": "^1.0.0",
5759
"@pnp/common": "2.5.0",
5860
"@pnp/odata": "2.5.0",
5961
"@pnp/sp": "2.5.0",

src/Calendar.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./controls/calendar";

src/controls/calendar/Calendar.tsx

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import * as React from 'react';
2+
3+
import {
4+
Body1,
5+
FluentProvider,
6+
IdPrefixProvider,
7+
webLightTheme,
8+
} from '@fluentui/react-components';
9+
import { addDays, startOfMonth, startOfWeek } from 'date-fns';
10+
import { useEffect, useRef, useState } from 'react';
11+
12+
import { Day } from './Day';
13+
import DayView from './DayView';
14+
import { ECalendarViews } from './models/ECalendarViews';
15+
import { ICalendarControlProps } from './models/ICalendarControlProps';
16+
import { ICalendarDay } from './models/ICalendarDay';
17+
import { IEvent } from './models/IEvents';
18+
import { Stack } from '@nuvemerudita/react-controls';
19+
import Toolbar from './Toolbar';
20+
import WeekView from './WeekView';
21+
import strings from 'ControlStrings';
22+
import { useCalendar } from './hooks/useCalendar';
23+
import { useCalendarStyles } from './hooks/useCalendarStyles';
24+
25+
interface CalendarState {
26+
currentDate: Date;
27+
}
28+
29+
const daysOfWeek: string[] = [
30+
strings.CalendarControlDayOfWeekSunday,
31+
strings.CalendarControlDayOfWeekMonday,
32+
strings.CalendarControlDayOfWeekTuesday,
33+
strings.CalendarControlDayOfWeekWednesday,
34+
strings.CalendarControlDayOfWeekThursday,
35+
strings.CalendarControlDayOfWeekFriday,
36+
strings.CalendarControlDayOfWeekSaturday,
37+
];
38+
39+
export const Calendar: React.FC<ICalendarControlProps> = (
40+
props: ICalendarControlProps
41+
) => {
42+
const { styles } = useCalendarStyles(props as never);
43+
const calendarRef = useRef<HTMLDivElement>(null);
44+
const [rowHeight, setRowHeight] = useState<number | null>(null);
45+
const {
46+
events,
47+
height = 800,
48+
theme,
49+
onDayChange,
50+
onMonthChange,
51+
onWeekChange,
52+
onViewChange,
53+
} = props;
54+
const rowHeightRef = useRef<number>(0);
55+
56+
// Default current date
57+
const [currentDate, setCurrentDate] = useState<CalendarState['currentDate']>(
58+
new Date()
59+
);
60+
// Default view
61+
const [selectedView, setSelectedView] = useState<ECalendarViews>(
62+
ECalendarViews.Month
63+
);
64+
// Get month calendar
65+
const { getMonthCalendar } = useCalendar(
66+
events,
67+
Intl.DateTimeFormat().resolvedOptions().timeZone
68+
);
69+
// Get calendar days
70+
const getCalendarDays = React.useCallback((date: Date): ICalendarDay[] => {
71+
const month = date.getMonth();
72+
const firstDayOfMonth = startOfMonth(date);
73+
const firstDayOfWeek = startOfWeek(firstDayOfMonth);
74+
const totalDisplayedDays = 42;
75+
// Generate calendar days
76+
const calendarDays = Array.from(
77+
{ length: totalDisplayedDays },
78+
(_, index): ICalendarDay => {
79+
const relativeDay = addDays(firstDayOfWeek, index);
80+
return {
81+
day: relativeDay.getDate(),
82+
currentMonth: relativeDay.getMonth() === month,
83+
date: relativeDay,
84+
};
85+
}
86+
);
87+
return calendarDays;
88+
}, []);
89+
90+
// Handle month change
91+
const handleMonthChange = React.useCallback(
92+
(date: Date): void => {
93+
if (onMonthChange) {
94+
onMonthChange(date);
95+
}
96+
setCurrentDate(date);
97+
},
98+
[currentDate]
99+
);
100+
// Handle day change
101+
const handleDayChange = React.useCallback(
102+
(date: Date): void => {
103+
if (onDayChange) {
104+
onDayChange(date);
105+
}
106+
setCurrentDate(date);
107+
},
108+
[currentDate]
109+
);
110+
// Handle week change
111+
const handleWeekChange = React.useCallback(
112+
(date: Date): void => {
113+
if (onWeekChange) {
114+
onWeekChange(date);
115+
}
116+
setCurrentDate(date);
117+
},
118+
[currentDate]
119+
);
120+
// Handle view change
121+
const handleViewChange = React.useCallback(
122+
(view: ECalendarViews): void => {
123+
if (onViewChange) {
124+
onViewChange(view);
125+
}
126+
setSelectedView(view);
127+
},
128+
[selectedView]
129+
);
130+
// Get events for the day
131+
const getEventsForDay = React.useCallback(
132+
(dateObj: ICalendarDay): IEvent[] => {
133+
const { date } = dateObj;
134+
const monthEvents = getMonthCalendar(
135+
currentDate.getFullYear(),
136+
currentDate.getMonth()
137+
);
138+
const dayString = date.toISOString().split('T')[0]; // Get date in YYYY-MM-DD format
139+
return monthEvents[dayString]?.flatMap((slot) => slot) || [];
140+
},
141+
[currentDate]
142+
);
143+
// Resize observer
144+
useEffect(() => {
145+
if (calendarRef.current) {
146+
const firstDataColumnCell = calendarRef.current.querySelector(
147+
`.${styles.calendarWrapper} > div:nth-child(8)`
148+
) as HTMLDivElement;
149+
150+
if (firstDataColumnCell) {
151+
setRowHeight(firstDataColumnCell.offsetHeight);
152+
rowHeightRef.current = firstDataColumnCell.offsetHeight;
153+
}
154+
}
155+
}, [calendarRef.current]);
156+
// Toolbar component
157+
158+
// Render Month View
159+
const RenderMonthView = React.useCallback(() => {
160+
return (
161+
<Stack
162+
height={height}
163+
width={'100%'}
164+
verticalAlign="start"
165+
horizontalAlign="center"
166+
>
167+
<div className={styles.calendarWrapper} ref={calendarRef}>
168+
{daysOfWeek.map((day) => (
169+
<Body1 key={day} className={styles.dayHeader}>
170+
{day}
171+
</Body1>
172+
))}
173+
{getCalendarDays(currentDate).map((dateObj, index) => {
174+
const { day, currentMonth } = dateObj;
175+
const events = currentMonth ? getEventsForDay(dateObj) : [];
176+
return (
177+
<Day
178+
key={index}
179+
day={day}
180+
date={dateObj.date}
181+
currentMonth={dateObj.currentMonth}
182+
events={events}
183+
columnHeight={rowHeight || 0}
184+
/>
185+
);
186+
})}
187+
</div>
188+
</Stack>
189+
);
190+
}, [currentDate, getCalendarDays, getEventsForDay, rowHeight, styles]);
191+
// Render Week View
192+
const RenderWeekView = React.memo(() => {
193+
return (
194+
<>
195+
{selectedView === ECalendarViews.Week && (
196+
<WeekView events={events} currentDay={currentDate} height={height} />
197+
)}
198+
</>
199+
);
200+
});
201+
// Render Day View
202+
const RenderDayView = React.memo(() => {
203+
return (
204+
<>
205+
{selectedView === ECalendarViews.Day && (
206+
<DayView currentDay={currentDate} events={events} height={height} />
207+
)}
208+
</>
209+
);
210+
});
211+
212+
// Render content
213+
const RenderContent = React.useCallback(() => {
214+
switch (selectedView) {
215+
case ECalendarViews.Month:
216+
return <RenderMonthView />;
217+
case ECalendarViews.Week:
218+
return <RenderWeekView />;
219+
case ECalendarViews.Day:
220+
return <RenderDayView />;
221+
default:
222+
return <RenderMonthView />;
223+
}
224+
}, [selectedView, RenderMonthView, RenderWeekView, RenderDayView]);
225+
226+
return (
227+
<>
228+
<IdPrefixProvider value="calendarControl-">
229+
<FluentProvider theme={theme ?? webLightTheme}>
230+
<Stack height={'100%'} verticalAlign="start">
231+
<Toolbar
232+
selectedView={selectedView}
233+
onSelectedView={handleViewChange}
234+
currentDate={currentDate}
235+
setCurrentDate={setCurrentDate}
236+
onWeekChange={handleWeekChange}
237+
onMonthChange={handleMonthChange}
238+
onDayChange={handleDayChange}
239+
/>
240+
<RenderContent />
241+
</Stack>
242+
</FluentProvider>
243+
</IdPrefixProvider>
244+
</>
245+
);
246+
};
247+
248+
export default Calendar;

src/controls/calendar/Day.tsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import * as React from "react";
2+
3+
import { Body2, mergeClasses } from "@fluentui/react-components";
4+
import { Card, Stack } from "@nuvemerudita/react-controls";
5+
6+
import { IEvent } from "./models/IEvents";
7+
import { RenderEventToDayOfMonth } from "./RenderEventToDayOfMonth";
8+
import { isSameDay } from "date-fns";
9+
import { useCalendarStyles } from "./hooks/useCalendarStyles";
10+
11+
export interface IDayProps {
12+
day: number;
13+
date: Date;
14+
currentMonth: boolean;
15+
events: IEvent[];
16+
columnHeight: number;
17+
}
18+
19+
export const Day: React.FunctionComponent<IDayProps> = (
20+
props: React.PropsWithChildren<IDayProps>
21+
) => {
22+
const { day, currentMonth, events, date, columnHeight } = props;
23+
const { styles } = useCalendarStyles(props);
24+
const currentDate = new Date();
25+
const [isEventHovered, setIsEventHovered] = React.useState(false);
26+
27+
const isCurrentDayAndMonth = React.useMemo(() => {
28+
return isSameDay(date, currentDate);
29+
}, [date, currentDate]);
30+
31+
const renderCurrentDayLabel = React.useMemo(() => {
32+
return (
33+
<Stack
34+
horizontal
35+
horizontalAlign="start"
36+
verticalAlign="center"
37+
RowGap={10}
38+
>
39+
<Body2 className={styles.currentDayLabel}>
40+
{day} {currentDate.toLocaleString("default", { month: "short" })}
41+
</Body2>
42+
</Stack>
43+
);
44+
}, [day]);
45+
46+
const handleCardHoverChange = React.useCallback(
47+
(isHovered: boolean): void => {
48+
setIsEventHovered(isHovered);
49+
},
50+
[]
51+
);
52+
53+
return (
54+
<>
55+
<Card
56+
className={mergeClasses(
57+
styles.cardDay,
58+
currentMonth ? "" : styles.otherMonthDay,
59+
isCurrentDayAndMonth ? styles.currentDay : "",
60+
!isEventHovered ? styles.cardDayOnHover : ""
61+
)}
62+
cardHeader={
63+
isCurrentDayAndMonth ? renderCurrentDayLabel : <Body2>{day}</Body2>
64+
}
65+
padding="xsmall"
66+
cardBody={
67+
<RenderEventToDayOfMonth
68+
events={events}
69+
date={date}
70+
onCardHoverChange={handleCardHoverChange}
71+
columnHeight={columnHeight - 60}
72+
/>
73+
}
74+
/>
75+
</>
76+
);
77+
};

0 commit comments

Comments
 (0)