Skip to content

Commit

Permalink
Use FullCalendar
Browse files Browse the repository at this point in the history
This commit introduces FullCalendar for our calendar's UI. It does not implement adding or removing absences from the calendar.
  • Loading branch information
ChinemeremChigbo committed Nov 25, 2024
1 parent 2ae1b52 commit 7300a3a
Show file tree
Hide file tree
Showing 5 changed files with 326 additions and 19 deletions.
53 changes: 52 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@faker-js/faker": "^8.4.1",
"@fullcalendar/core": "^6.1.15",
"@fullcalendar/daygrid": "^6.1.15",
"@fullcalendar/interaction": "^6.1.15",
"@fullcalendar/react": "^6.1.15",
"@mui/material": "^5.15.20",
"@mui/system": "^5.15.20",
"@prisma/client": "^5.17.0",
Expand Down
61 changes: 61 additions & 0 deletions src/components/CalendarHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';
import {
ButtonGroup,
Button,
IconButton,
Flex,
Heading,
useTheme,
} from '@chakra-ui/react';
import {
ArrowBackIcon,
ArrowForwardIcon,
CalendarIcon,
} from '@chakra-ui/icons';

interface CalendarHeaderProps {
currentMonthYear: string;
onTodayClick: () => void;
onPrevClick: () => void;
onNextClick: () => void;
}

const CalendarHeader: React.FC<CalendarHeaderProps> = ({
currentMonthYear,
onTodayClick,
onPrevClick,
onNextClick,
}) => {
const theme = useTheme();
return (
<Flex marginBottom={theme.space[4]} alignItems="center">
<ButtonGroup isAttached variant="outline">
<IconButton
colorScheme="blue"
onClick={onPrevClick}
icon={<ArrowBackIcon />}
aria-label="Previous"
/>
<Button
onClick={onTodayClick}
variant="outline"
colorScheme="blue"
leftIcon={<CalendarIcon />}
>
Today
</Button>
<IconButton
colorScheme="blue"
onClick={onNextClick}
icon={<ArrowForwardIcon />}
aria-label="Next"
/>
</ButtonGroup>
<Heading fontSize="xl" textAlign="center" marginX={theme.space[6]}>
{currentMonthYear}
</Heading>
</Flex>
);
};

export default CalendarHeader;
27 changes: 27 additions & 0 deletions src/components/CalendarSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { Flex, Box, Button, useTheme } from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons';
import { SistemaLogoColour } from '../components/SistemaLogoColour';

const CalendarSidebar: React.FC = () => {
const theme = useTheme();

return (
<Flex
width="260px"
padding={theme.space[4]}
flexDirection="column"
gap={theme.space[4]}
alignItems="center"
>
<Box width="150px">
<SistemaLogoColour />
</Box>
<Button colorScheme="blue" size="lg" leftIcon={<AddIcon />}>
Declare Absence
</Button>
</Flex>
);
};

export default CalendarSidebar;
200 changes: 182 additions & 18 deletions src/pages/calendar.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,195 @@
import { Calendar as ReactCalendar } from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
import React, { useRef, useState, useEffect, useCallback } from 'react';
import FullCalendar from '@fullcalendar/react';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import { Box, Flex, useToast, useTheme } from '@chakra-ui/react';
import { EventInput, EventContentArg } from '@fullcalendar/core';
import { AbsenceWithRelations } from '../../app/api/getAbsences/route';
import Sidebar from '../components/CalendarSidebar';
import CalendarHeader from '../components/CalendarHeader';
import { Global } from '@emotion/react';

import { Container, Text } from '@chakra-ui/react';
const Calendar: React.FC = () => {
const calendarRef = useRef<FullCalendar>(null);
const [events, setEvents] = useState<EventInput[]>([]);
const [currentMonthYear, setCurrentMonthYear] = useState('');
const toast = useToast();
const theme = useTheme();

import React, { useState } from 'react';
const renderEventContent = useCallback(
(eventInfo: EventContentArg) => (
<Box>
<Box className="fc-event-title-container">
<Box className="fc-event-title fc-sticky">
{eventInfo.event.title}
</Box>
</Box>
<Box className="fc-event-title fc-sticky">
{eventInfo.event.extendedProps.location}
</Box>
</Box>
),
[]
);

type ValuePiece = Date | null;
const convertAbsenceToEvent = (
absenceData: AbsenceWithRelations
): EventInput => ({
title: absenceData.subject.name,
start: absenceData.lessonDate,
allDay: true,
display: 'auto',
location: absenceData.location.name,
});

type Value = ValuePiece | [ValuePiece, ValuePiece];
const fetchAbsences = useCallback(async () => {
try {
const res = await fetch('/api/getAbsences/');
if (!res.ok) {
throw new Error(`Failed to fetch: ${res.statusText}`);
}
const data = await res.json();
const formattedEvents = data.events.map(convertAbsenceToEvent);
setEvents(formattedEvents);
} catch (error) {
console.error('Error fetching absences:', error);
toast({
title: 'Failed to fetch absences',
description:
'There was an error loading the absence data. Please try again later.',
status: 'error',
duration: 5000,
isClosable: true,
});
}
}, [toast]);

const Calendar = () => {
const [value, onChange] = useState<Value>(new Date());
const [clickedDate, setClickedDate] = useState<ValuePiece>(null);
useEffect(() => {
fetchAbsences();
}, [fetchAbsences]);

const onClickDay = (value: ValuePiece) => {
setClickedDate(value);
const formatMonthYear = (date: Date): string => {
const options: Intl.DateTimeFormatOptions = {
month: 'long',
year: 'numeric',
};
return new Intl.DateTimeFormat('en-US', options).format(date);
};

const updateMonthYearTitle = useCallback(() => {
if (calendarRef.current) {
const calendarApi = calendarRef.current.getApi();
const date = calendarApi.getDate();
setCurrentMonthYear(formatMonthYear(date));
}
}, []);

const handleTodayClick = useCallback(() => {
if (calendarRef.current) {
const calendarApi = calendarRef.current.getApi();
calendarApi.today();
updateMonthYearTitle();
}
}, [updateMonthYearTitle]);

const handlePrevClick = useCallback(() => {
if (calendarRef.current) {
const calendarApi = calendarRef.current.getApi();
calendarApi.prev();
updateMonthYearTitle();
}
}, [updateMonthYearTitle]);

const handleNextClick = useCallback(() => {
if (calendarRef.current) {
const calendarApi = calendarRef.current.getApi();
calendarApi.next();
updateMonthYearTitle();
}
}, [updateMonthYearTitle]);

useEffect(() => {
updateMonthYearTitle();
}, [updateMonthYearTitle]);

const addWeekendClass = (date: Date): string => {
const day = date.getDay();
return day === 0 || day === 6 ? 'fc-weekend' : '';
};

return (
<Container>
<ReactCalendar
onChange={onChange}
value={value}
onClickDay={onClickDay}
<>
<Global
styles={`
.fc .fc-daygrid-day-top {
flex-direction: row;
}
.fc th {
text-transform: uppercase;
font-size: ${theme.fontSizes.sm};
font-weight: ${theme.fontWeights.normal};
}
.fc-day-today {
background-color: inherit !important;
}
.fc-daygrid-day-number {
margin-left: 7px;
margin-top: 5px;
font-size: ${theme.fontSizes.md};
font-weight: ${theme.fontWeights.normal};
}
.fc-day-today .fc-daygrid-day-number {
background-color: ${theme.colors.blue[500]};
color: white;
border-radius: 50%;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.fc-weekend {
background-color: rgba(0, 0, 0, 0.05) !important;
}
.fc-event {
padding: ${theme.space[2]} ${theme.space[3]};
margin: ${theme.space[2]} 0;
border-radius: ${theme.radii.md};
}
.fc-event-title {
overflow: hidden;
text-overflow: ellipsis;
font-size: ${theme.fontSizes.sm};
font-weight: ${theme.fontWeights.normal};
}
`}
/>
<Text>Clicked date: {clickedDate?.toDateString()}</Text>
</Container>

<Flex height="100vh">
<Sidebar />
<Box flex={1} padding={theme.space[4]} height="100%">
<CalendarHeader
currentMonthYear={currentMonthYear}
onTodayClick={handleTodayClick}
onPrevClick={handlePrevClick}
onNextClick={handleNextClick}
/>
<FullCalendar
ref={calendarRef}
headerToolbar={false}
plugins={[dayGridPlugin, interactionPlugin]}
initialView="dayGridMonth"
height="100%"
events={events}
eventContent={renderEventContent}
timeZone="local"
datesSet={updateMonthYearTitle}
fixedWeekCount={false}
dayCellClassNames={({ date }) => addWeekendClass(date)}
/>
</Box>
</Flex>
</>
);
};

Expand Down

0 comments on commit 7300a3a

Please sign in to comment.