While recently working on a full-stack development project, I got to experience the usefulness of React Big-Calendar. My aim was to create a scheduling application for a home health nurse that would improve their ability to logistically plan their day. This involved creating a calendar that would hold and update current events. The inspiration for the application came from my years working in the home care industry. I wanted the user to be able to have the functionality of a home care scheduling application while also being easy to use.
To start using react big-calendar we can install by running this on your client side:
npm install react-big-calendar moment @chakra-ui/react
You will need to initialize your application inside of a page or a component. I initialized mine in a component called UserCalendar. Below is a reduced version of my code to get started. You will have many options to tailor the calendar as you build.
import React, { useEffect, useState } from "react";
import { Box, Button, useDisclosure, Select, Badge, Stack } from "@chakra-ui/react";
import { Calendar, momentLocalizer, Views } from "react-big-calendar";
import moment from "moment";
import "react-big-calendar/lib/css/react-big-calendar.css";
const localizer = momentLocalizer(moment);
const EVENT_STATUS_COLORS = { 1: "gray", 2: "green", 3: "yellow", 4: "blue", 5: "magenta" };
const UserCalendar = () => {
const [events, setEvents] = useState([]);
const { isOpen, onOpen, onClose } = useDisclosure();
useEffect(() => {
fetch("/api/events")
.then(response => response.json())
.then(data => setEvents(data.map(event => ({
...event, start: new Date(event.start), end: new Date(event.end)
}))));
}, []);
return (
<Box>
<Button onClick={onOpen}>Add Event</Button>
<Stack direction="row" mb={4}>
{Object.entries(EVENT_STATUS_COLORS).map(([status, color]) => (
<Badge key={status} colorScheme={color}>{status}</Badge>
))}
</Stack>
<Calendar
localizer={localizer}
events={events}
startAccessor="start"
endAccessor="end"
style={{ height: 500 }}
views={[Views.MONTH, Views.WEEK, Views.DAY, Views.AGENDA]}
eventPropGetter={event => ({
style: { backgroundColor: EVENT_STATUS_COLORS[event.status] }
})}
/>
</Box>
);
};
export default UserCalendar;
You should now have a calendar rendered on your page that allows you to switch between monthly, weekly, daily and agenda views. The date picker and buttons labeled Today, Next and Back will be present as part of Big Calendar as well. However, you need to get something on the calendar. There is no built in form to add events, however, the calendar will react to selecting a slot in the calendar. I set mine to open an event form that would handle creating simple events to get started. Once again, this code is only a parital representation of my current event form.
import React from "react";
import { Formik, Form, Field } from "formik";
import { Box, Button, FormControl, FormLabel, Input, Textarea, Switch } from "@chakra-ui/react";
const EventForm = ({ isOpen, onClose, onSubmit, initialValues }) => {
return (
<Formik
initialValues={initialValues}
onSubmit={onSubmit}
>
{({ values, setFieldValue }) => (
<Form>
<FormControl>
<FormLabel>Notes</FormLabel>
<Field name="notes" as={Textarea} />
</FormControl>
<FormControl>
<FormLabel>Start</FormLabel>
<Field name="start" type="datetime-local" as={Input} />
</FormControl>
<FormControl>
<FormLabel>End</FormLabel>
<Field name="end" type="datetime-local" as={Input} />
</FormControl>
<FormControl>
<FormLabel>Recurring</FormLabel>
<Field name="is_recurring" type="checkbox" as={Switch} />
</FormControl>
<Button type="submit">Save</Button>
</
This snippet allows the user to enter in a start time and an end time on the event form, which is most of what you will need to get started. I used Chakra UI to help model the forms and provide modality later on. I believe it provided a nice result. Depending on your needs, you can customize your event form to present a lot of information to the user. My event form included fields for the event_status, event_type, add_address, event_address, start_time, end_time, client_name, recurrence_rule, is_recurring, notes, user_id, client_id and parent_event_id. Actually, I have more, but there's already enough to write about. By selecting add address or is recurring, additional fields open up on the event form. This can get very involved quickly, so make decisions on your backend wisely.
Creating Recurring Events
To create recurring events, you need to understand the recurrence rule.
DTSTART:20240601T100000Z
RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE;UNTIL=20241231T235959Z
Explanation
FREQ=WEEKLY: This indicates that the frequency of the recurrence is weekly.
INTERVAL=2: This specifies that the event should occur every 2 weeks.
BYDAY=MO,WE: This indicates that the event should occur on Mondays and Wednesdays.
DTSTART:20240601T100000Z: This specifies the start date and time of the first occurrence, in UTC format (June 1, 2024, at 10:00 UTC).
UNTIL=20241231T235959Z: This specifies the end date and time for the recurrence rule, in UTC format (December 31, 2024, at 23:59 UTC).
With this recurrence rule, the event will repeat every other week on Mondays and Wednesdays from June 1, 2024, until December 31, 2024.
So in order to create recurring events, I needed to make a function to create the recurrence rule, read/parse so it was viewable and the method of selection on my form. By the way, my application uses the event date, so I did not need to use a DTSTART, but thought it would be nice to put in this example.
Method to Generate Recurrence Rule
import { RRule, rrulestr } from 'rrule';
const createRRule = (values) => {
let rrule = "";
if (values.is_recurring) {
if (values.recurrence_option === "EOW") {
rrule = `FREQ=WEEKLY;INTERVAL=2;BYDAY=${values.recurrence_days.join(",")}`;
} else if (values.recurrence_option === "Monthly") {
const byDayValues = values.recurrence_weeks.map(week =>
values.recurrence_days.map(day => `${week}${day}`).join(",")
).join(",");
rrule = `FREQ=MONTHLY;BYDAY=${byDayValues}`;
} else if (values.recurrence_option === "Weekly") {
rrule = `FREQ=WEEKLY;BYDAY=${values.recurrence_days.join(",")}`;
}
if (values.recurrence_end) {
rrule += `;UNTIL=${values.recurrence_end.replace(/-/g, "")}T235959Z`;
}
}
return rrule;
};
Method to Parse Recurrence Rule
const parseRecurrenceRule = (recurrenceRule, setFieldValue) => {
if (!recurrenceRule) return;
const parts = recurrenceRule.split(";");
let recurrenceDays = [];
let recurrenceEnd = "";
let recurrenceOption = "";
let recurrenceWeeks = [];
parts.forEach((part) => {
if (part.startsWith("FREQ=")) {
const freq = part.replace("FREQ=", "");
if (freq === "WEEKLY") {
if (part.includes("INTERVAL=2")) {
recurrenceOption = "EOW";
} else {
recurrenceOption = "Weekly";
}
} else if (freq === "MONTHLY") {
recurrenceOption = "Monthly";
}
} else if (part.startsWith("BYDAY=")) {
const days = part.replace("BYDAY=", "").split(",");
if (recurrenceOption === "Monthly") {
days.forEach(day => {
const week = day[0];
const dayOfWeek = day.substring(1);
recurrenceWeeks.push(parseInt(week, 10));
recurrenceDays.push(dayOfWeek);
});
} else {
recurrenceDays = days;
}
} else if (part.startsWith("UNTIL=")) {
const untilDate = part.replace("UNTIL=", "");
recurrenceEnd = `${untilDate.substring(0, 4)}-${untilDate.substring(4, 6)}-${untilDate.substring(6, 8)}`;
}
});
setFieldValue("recurrence_days", [...new Set(recurrenceDays)]);
setFieldValue("recurrence_end", recurrenceEnd);
setFieldValue("recurrence_option", recurrenceOption);
setFieldValue("recurrence_weeks", [...new Set(recurrenceWeeks)]);
};
Formula to Generate Recurring Events
const generateRecurringEvents = (event) => {
if (!event || !event.start) {
console.error("Invalid event data passed to generateRecurringEvents:", event);
return [];
}
const maxEndDate = new Date(event.start);
maxEndDate.setDate(maxEndDate.getDate() + 180); // 180 days from start date
const rule = rrulestr(event.recurrence_rule, { dtstart: new Date(event.start) });
const occurrences = rule.between(new Date(event.start), maxEndDate, true); // Generate all occurrences
const recurringEvents = occurrences.map((occurrence) => {
if (occurrence > new Date(event.start)) {
const end = new Date(occurrence);
end.setTime(end.getTime() + (new Date(event.end) - new Date(event.start))); // Adjust end time
return {
start: occurrence.toISOString(),
end: end.toISOString(),
type: event.type,
status: event.status,
is_fixed: event.is_fixed,
priority: event.priority,
is_recurring: false, // Each generated event is not recurring itself
recurrence_rule: event.recurrence_rule, // Include the recurrence rule
notify_client: event.notify_client,
notes: event.notes,
is_completed: event.is_completed,
is_endpoint: event.is_endpoint,
address: event.address,
city: event.city,
state: event.state,
zip: event.zip,
user_id: event.user_id,
user_client_id: event.user_client_id,
parent_event_id: event.id, // Link to the original event
};
}
return null;
}).filter(event => event !== null);
return recurringEvents;
};
By using something similar to this you can easily create recurring events. I elected to have buttons appear to match the needs of the recurrence rule: days, weeks, options. By storing all of the data in the RRule, and then using the formula to parse it, I saved having to create extra elements in the events table.
Updating a Series of Events
Updating a series of events involves not only creating events but, a little bit more. I elected to find all events after the date of the selected event based off the parent_event_id created when the event was initialized. Store those events. Create new events. Delete the stored events. Why not patch? I think that if certain modifications were to be made to the recurrence rule, like adding a day to the rule, would mean that a post and a patch would need to be done at the same time. For the same amount of CRUD actions, I thought this approach was cleaner.
Switching Between My View and Client View
One nice feature I found in Big Calendar was that I could switch views from the user view, to the client view, and only see events tied to a selected client. I originally thought I was going to have to create a separate component, but the calendar took care of it for me with a little added code:
const [view, setView] = useState("my");
const [selectedClient, setSelectedClient] = useState(null);
const toggleView = () => {
setView(view === "my" ? "client" : "my");
setSelectedClient(null); // Reset selected client when switching views
};
const filteredEvents = events.filter(event => {
if (view === "my") {
return event.type !== 3; // Exclude "Client Unavailable" events
} else if (view === "client") {
return event.type !== 2 && event.user_client_id === parseInt(selectedClient);
}
return true;
});
Handling Overdue and Overlapping Events
I was able to assign colors to my events based on their status (pending, confirmed, completed,cancelled), but I wanted the user to see visually events that were conflicted or overdue. By modifying the UserCalendar we can achieve this. Here is my method:
const eventPropGetter = (event) => {
const isOverdue = !event.is_completed && new Date() > new Date(event.end);
const overlappingIds = findOverlappingEvents(events);
const isOverlap = overlappingIds.includes(event.id);
const backgroundColor = isOverdue ? "red" : (isOverlap ? "orange" : EVENT_STATUS_COLORS[event.status]);
return { style: { backgroundColor } };
};
const findOverlappingEvents = (events) => {
const overlaps = [];
for (let i = 0; i < events.length; i++) {
for (let j = i + 1; j < events.length; j++) {
if (new Date(events[i].end) > new Date(events[j].start) && new Date(events[i].start) < new Date(events[j].end)) {
overlaps.push(events[i].id);
overlaps.push(events[j].id);
}
}
}
return overlaps;
};
Practical User Agenda
If you are picky, like me, you may not like the agenda view in Big Calendar. I only covers a range of dates and the daily view page was great for creating events as a tool, but not what I wanted the user to see. By creating a User Agenda that listed the events of the day in order, with helpful links to the event, client profile, directions, event type and notes. The user can view the associated event forms, pick dates and add events. I set my agenda to fetch the data from events like this:
const DailyAgenda = ({ userId, selectedDate, onDateChange, onPreviousDay, onNextDay }) => {
const [events, setEvents] = useState([]);
const [clients, setClients] = useState([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedEvent, setSelectedEvent] = useState(null);
const navigate = useNavigate();
useEffect(() => {
const fetchEvents = async () => {
try {
const response = await fetch(`/api/users/${userId}/events?date=${selectedDate.toISOString().split('T')[0]}`);
let eventsData = await response.json();
eventsData = eventsData.filter(event => event.type !== 3);
const clientDetails = await Promise.all(eventsData.map(async (event) => {
const clientResponse = await fetch(`/api/user_clients/${event.user_client_id}`);
const clientData = await clientResponse.json();
return {
...event,
client_address: clientData.address_line_1,
client_city: clientData.city,
client_state: clientData.state,
client_zip: clientData.zip,
};
}));
setEvents(clientDetails);
} catch (error) {
console.error('Error fetching events:', error);
}
};
const fetchClients = async () => {
try {
const response = await fetch(`/api/user_clients`);
const clientsData = await response.json();
setClients(clientsData);
} catch (error) {
console.error('Error fetching clients:', error);
}
};
fetchEvents();
fetchClients();
}, [selectedDate, userId]);
I rendered the agenda incorporating links like this:
const handleOpenModal = (event) => {
setSelectedEvent(event);
setIsModalOpen(true);
};
const handleCloseModal = () => {
setSelectedEvent(null);
setIsModalOpen(false);
};
return (
<Box>
<Flex justifyContent="space-between" alignItems="center" mb={4}>
<Button colorScheme="blue" onClick={() => handleOpenModal(null)}>
Add Event
</Button>
<Text fontSize="2xl" fontWeight="bold">Daily Agenda</Text>
<Flex alignItems="center">
<Button onClick={onPreviousDay}>Previous</Button>
<DatePicker
selected={selectedDate}
onChange={onDateChange}
dateFormat="yyyy-MM-dd"
customInput={<Button>{format(selectedDate, 'yyyy-MM-dd')}</Button>}
/>
<Button onClick={onNextDay}>Next</Button>
</Flex>
</Flex>
<Text fontSize="lg" fontWeight="bold" textAlign="center" mb={4}>
{format(selectedDate, 'EEEE, MMMM d, yyyy')}
</Text>
<Table variant="striped" colorScheme="teal">
<Thead>
<Tr>
<Th>Time</Th>
<Th>Client Name</Th>
<Th>Address</Th>
<Th>Type</Th>
<Th>Notes</Th>
</Tr>
</Thead>
<Tbody>
{events.map(event => (
<Tr key={event.id}>
<Td>
<Button variant="link" onClick={() => handleOpenModal(event)}>
{`${new Date(event.start).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} – ${new Date(event.end).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`}
</Button>
</Td>
<Td>
<Button variant="link" onClick={() => navigate(`/usermenu/${userId}/clients/${event.user_client_id}`)}>
{event.client_name}
</Button>
</Td>
<Td>
<Button
variant="link"
onClick={() => window.open(`https://www.google.com/maps/dir/?api=1&destination=${event.address || `${event.client_address}, ${event.client_city}, ${event.client_state}, ${event.client_zip}`}`, '_blank')}
>
{event.address || `${event.client_address}, ${event.client_city}, ${event.client_state}, ${event.client_zip}`}
</Button>
</Td>
<Td>{EVENT_TYPE_MAP[event.type]}</Td>
<Td>{event.notes.slice(0, 20)}{event.notes.length > 20 && '...'}</Td>
</Tr>
))}
</Tbody>
</Table>
<Modal isOpen={isModalOpen} onClose={handleCloseModal}>
<ModalOverlay />
<ModalContent>
<ModalHeader>{selectedEvent ? 'Edit Event' : 'Add Event'}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<EventForm event={selectedEvent} clients={clients} onClose={handleCloseModal} />
</ModalBody>
</ModalContent>
</Modal>
</Box>
);
};
With a little bit a coding you can create an easy to use agenda for your users.
Conclusion
Creating a user friendly application in Big Calendar is not only possible, but with enough tailoring, you can make an excellent professional scheduling application that will work similarly to one's companies pay lots of money for their employees to use. Even better, React Big Calendar is free. So give it a try. Hope you enjoyed this article. If you have any questions or comments, let me know. Take Care.
Top comments (0)