Tired of calendars that just show dates? In React projects demanding more than the basics, features like email notifications and business hours are crucial. They transform a simple calendar into a professional and efficient scheduling tool.
In this tutorial, we'll dive into the React Agendfy Calendar, a React component that offers these advanced features in a customizable way. Get ready to create smart schedules that remind your users of appointments and respect working hours! ๐
Step 1: Setting the Stage - Installation and Imports
First, we need to set up our environment. To follow this tutorial, you should already have a React project running. If not, quickly create a new React project using Create React App or Vite.
With your React project ready, install the necessary packages:
npm install react-agendfy formik yup react-modal
# or with yarn
yarn add react-agendfy formik yup react-modal
What did we install and why?
-
react-agendfy
: The heart of our calendar, the main component. -
formik
andyup
: To efficiently create and validate forms (we'll use a form to add events). -
react-modal
: To create a simple modal where our event form will reside.
Now, in your App.js
file (or your preferred component file), add the necessary imports:
import React, { useState, useEffect } from "react";
import { Calendar } from "react-agendfy";
import { Formik, Form, Field, ErrorMessage, useFormikContext } from "formik";
import * as Yup from "yup";
import Modal from "react-modal";
import "react-agendfy/dist/react-agendfy.css";
Modal.setAppElement("#root");
Explaining the Imports:
-
React, { useState, useEffect }
: Basic React imports to create functional components and use hooks to manage state and side effects. -
{ Calendar } from "react-agendfy"
: Imports theCalendar
component fromreact-agendfy
. This is the component that will render the calendar in our application. -
{ Formik, Form, Field, ErrorMessage, useFormikContext } from "formik"
: Imports fromformik
, a fantastic library for handling forms in React. We will use:-
Formik
: The main Formik component, which wraps our form. -
Form
: A component representing the HTML<form>
element. -
Field
: A component for each form field (inputs, selects, etc.). -
ErrorMessage
: A component to display validation error messages. -
useFormikContext
: A hook to access the Formik context within child components (we'll use it inFormFields
).
-
-
* as Yup from "yup"
: Imports theyup
library, which we will use to define validation rules for our form (e.g., ensuring required fields are filled). -
Modal from "react-modal"
: Imports theModal
component fromreact-modal
, to create modal windows (pop-ups) for adding new events to the calendar. -
"react-agendfy/dist/react-agendfy.css"
: Crucial! Imports thereact-agendfy
CSS file to apply the default calendar styles and ensure it looks as expected. -
Modal.setAppElement("#root");
:react-modal
configuration for accessibility. Ensures the modal works correctly with screen readers and other assistive technologies by associating the modal with the root element of the application (<div id="root">
).
Step 2: Defining Resources and Initial Events
Before diving into calendar configuration, let's create some example data: resources and initial events. Resources represent what can be scheduled (rooms, people, equipment), and events are the schedules themselves.
Add these constants within your App
component:
const resources = [
{ id: "r1", name: "Conference Room", type: "room" },
{ id: "r2", name: "John Doe", type: "person" },
{ id: "r3", name: "Projector", type: "equipment" },
{ id: "r4", name: "Holidays", type: "category" },
];
const initialEvents = [
{
id: "1",
title: "Planning Meeting",
start: "2025-03-01T09:00:00.000Z",
end: "2025-03-01T10:00:00.000Z",
color: "#3490dc",
isAllDay: false,
isMultiDay: false,
resources: [{ id: "r1", name: "Conference Room", type: "room" }],
},
{
id: "10",
title: "National Holiday",
start: "2025-03-02T06:00:00.000Z",
end: "2025-03-02T21:59:59.000Z",
color: "#ff9800",
isAllDay: false,
isMultiDay: true,
resources: [{ id: "r4", name: "Holidays", type: "category" }],
},
{
id: "4",
title: "Daily Standup",
start: "2025-03-06T04:00:00.000-03:00",
end: "2025-03-06T06:15:00.000-03:00",
color: "#4caf50",
recurrence: "FREQ=WEEKLY;INTERVAL=1;COUNT=10",
isAllDay: false,
isMultiDay: false,
resources: [],
},
{
id: "5",
title: "Carnival",
start: "2025-03-01T10:00:00.000Z",
end: "2025-03-02T12:15:00.000Z",
color: "#a10861",
isAllDay: true,
isMultiDay: true,
},
];
Resources (resources
):
-
resources
is an array of objects, where each object represents a resource that can be scheduled or associated with an event. -
id
: A unique identifier for the resource (string). -
name
: The display name of the resource (string). -
type
: The type or category of the resource (string). We use "room", "person", "equipment", and "category" as examples, but you can define the types that make sense for your application.
Initial Events (initialEvents
):
-
initialEvents
is an array of objects, each representing an event on the calendar. -
id
: Unique event identifier (string). -
title
: Event title (string), displayed on the calendar. -
start
: Event start date and time, in ISO 8601 UTC format (string). Important:react-agendfy
expects dates in this format to ensure correct functioning with time zones. Example:"2025-03-01T09:00:00.000Z"
. -
end
: Event end date and time, also in ISO 8601 UTC format (string). -
color
: Event color (string, optional). Defines the background color of the event on the calendar. Ex:" #3490dc"
. -
isAllDay
: Optional boolean,true
if the event lasts all day,false
otherwise (default:false
). -
isMultiDay
: Optional boolean,true
if the event lasts more than one day,false
otherwise (default:false
). -
resources
: Optional array of resources associated with the event. Uses the same object structure asresources
, withid
,name
, andtype
. -
recurrence
: Optional string in RRule format, to define recurring events (that repeat). In the example, "Daily Standup" is a weekly event that repeats 10 times. -
alertBefore
: The key property for notifications! Defines a reminder, in minutes, before the event starts (number, optional). In the example, we don't usealertBefore
ininitialEvents
, but we will add it to events created by the form.
The initialEvents
already exemplify several react-agendfy
features, such as all-day events, multi-day events, recurring events, and resource association. Now let's configure the calendar itself, focusing on business hours and notifications.
Step 3: Configuring the Calendar - Business Hours and Notifications in Focus
The calendar component configuration is done through the config
prop, a Javascript object where we customize the behavior and appearance of the calendar. Let's create a config
object with a special focus on businessHours
(working hours) and alerts
(notifications).
Add the config
constant in your App
component:
const config = {
timeZone: 'Africa/Lagos', // Calendar timezone
defaultView: "day", // Initial calendar view
slotDuration: 30, // Time slot duration (minutes)
slotLabelFormat: "HH:mm", // Time slot label format
slotMin: "08:00", // Minimum hour displayed in slots
slotMax: "24:00", // Maximum hour displayed in slots
lang: "en", // Language of the calendar (standard texts)
today: "Today", // Customized texts for localization
monthView: "Month",
weekView: "Week",
dayView: "Day",
listView: "List",
filter_resources: "Filter Resources",
clear_filter: "Clear Filter",
businessHours: { // Business hours configuration
enabled: true, // Enables business hours highlighting
intervals: [ // Defines business hour intervals
{
daysOfWeek: [1, 2, 3, 4, 5], // Days of the week (1=Monday, 2=Tuesday, ..., 6=Saturday, 0=Sunday)
startTime: "09:00", // Work start time
endTime: "17:00", // Work end time
},
{
daysOfWeek: [6], // Saturday
startTime: "10:00",
endTime: "14:00",
},
],
},
alerts: { // Alerts/notifications configuration
enabled: true,
thresholdMinutes: 15,
},
};
Detailing the Configuration (config
) - Focus on businessHours
and alerts
:
-
timeZone
: Defines the calendar's main timezone. Crucial to ensure event times are displayed correctly for users in different locations. Use IANA timezone names (e.g.,'America/Sao_Paulo'
,'UTC'
,'Africa/Lagos'
). In the example, we use'Africa/Lagos'
. -
businessHours
: This section configures the visual highlighting of business hours in the calendar, mainly in week and day views.-
enabled: true
: Enables the business hours feature. Iffalse
, no highlighting will be displayed. -
intervals
: An array that allows defining multiple business hour intervals. Useful for companies with complex schedules or lunch breaks. Each interval is an object with:-
daysOfWeek
: An array of numbers representing the days of the week this interval applies to (0
= Sunday,1
= Monday, ...,6
= Saturday). In the example,[1, 2, 3, 4, 5]
represents Monday to Friday. -
startTime
: Work start time, in"HH:mm"
format (string). Ex:"09:00"
for 9 AM. -
endTime
: Work end time, in"HH:mm"
format (string). Ex:"17:00"
for 5 PM. In the example, we configured the default working hours from 9 AM to 5 PM, Monday to Friday, and from 10 AM to 2 PM on Saturdays. Sundays have no working hours defined (by default, they will not be highlighted).
-
-
-
alerts
: This section configures event notifications/alerts.-
enabled: true
: Enables the alert system. Iffalse
, thealertBefore
property in events will be ignored, and no alerts will be triggered. -
thresholdMinutes: 15
: Defines the default time (in minutes) before an event's start time for an alert to be considered "due" (and potentially triggered). In the example, 15 minutes. This is just a default value; you can customizealertBefore
individually in each event object (as we already saw ininitialEvents
).
-
The other properties in config
(such as defaultView
, slotDuration
, lang
, localization texts) are for general calendar customization and have been explained in the previous tutorial. The focus here is on businessHours
and alerts
.
Step 4: Building the App
Component - State Management and Handlers
Now let's build the App
component itself, step by step. We'll start with state management and functions (handlers) to handle user interactions with the calendar and form.
function App() {
const [events, setEvents] = useState(() => {
const storedEvents = localStorage.getItem("calendarEvents_dev");
return storedEvents ? JSON.parse(storedEvents) : initialEvents;
});
const [filteredResources, setFilteredResources] = useState([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedSlot, setSelectedSlot] = useState(null);
useEffect(() => {
localStorage.setItem("calendarEvents_dev", JSON.stringify(events));
}, [events]);
const updateEvents = (updatedEventOrEvents) => { /* ... implementation below ... */ };
const handleEventUpdate = (updatedEventOrEvents) => { /* ... implementation below ... */ };
const handleEventResizeUpdate = (updatedEventOrEvents) => { /* ... implementation below ... */ };
const handleSlotClick = (slotTime) => { /* ... implementation below ... */ };
const handleResourceFilterChange = (selectedResources) => { /* ... implementation below ... */ };
const handleModalClose = () => { /* ... implementation below ... */ };
const myEmailAdapter = { /* ... implementation below ... */ };
const handleFormSubmit = (values, { resetForm }) => { /* ... implementation below ... */ };
const FormFields = () => { /* ... implementation below ... */ };
return (
<div className="flex">
{/* ... JSX for the calendar and form (step 5) ... */}
</div>
);
}
State Management (useState
):
-
const [events, setEvents] = useState(...)
: This is the main state!events
stores the array of events that will be displayed on the calendar. We useuseState
to create this state andsetEvents
to update it.- The initial value of
events
is obtained fromlocalStorage
(if there are events saved there, for persistence between sessions) or frominitialEvents
(if there is nothing inlocalStorage
or the first time the application runs).
- The initial value of
-
const [filteredResources, setFilteredResources] = useState([])
: State to control the resources that are being filtered in the calendar. Initially empty (no active filter). -
const [isModalOpen, setIsModalOpen] = useState(false)
: Boolean state to control the visibility of the event creation modal. Initially closed (false
). -
const [selectedSlot, setSelectedSlot] = useState(null)
: State to store the time slot (date and time) that the user clicked on the calendar to add a new event. Initiallynull
.
Side Effect (useEffect
):
-
useEffect(() => { localStorage.setItem("calendarEvents_dev", JSON.stringify(events)); }, [events]);
: ThisuseEffect
ensures that events are saved inlocalStorage
whenever theevents
state is updated. This allows events to persist even after the user closes and reopens the page (simple persistence in the browser).
Handler Functions (user interaction and event handlers):
-
const updateEvents = (updatedEventOrEvents) => { ... }
: Utility function to update theevents
state with one or more updated events. It receives an event or an array of events, and updates theevents
array in the state, keeping events that have not been changed and adding new events if needed. -
const handleEventUpdate = (updatedEventOrEvents) => { updateEvents(updatedEventOrEvents); };
: Handler for theCalendar
'sonEventUpdate
event. Called when an event is moved (drag and drop) or resized on the calendar. It simply forwards the updated event toupdateEvents
to update the state. -
const handleEventResizeUpdate = (updatedEventOrEvents) => { ... }
: Handler for theCalendar
'sonEventResize
event. Called when an event is resized (by dragging the border). Similar tohandleEventUpdate
, it updates the state withupdateEvents
. -
const handleSlotClick = (slotTime) => { setSelectedSlot(slotTime); setIsModalOpen(true); };
: Handler for theCalendar
'sonSlotClick
event. Called when the user clicks on an empty slot on the calendar. Sets theselectedSlot
to the clicked time and opens the event creation modal (setIsModalOpen(true)
). -
const handleResourceFilterChange = (selectedResources) => { setFilteredResources(selectedResources); };
: Handler for theCalendar
'sonResourceFilterChange
event. Called when the resource filter changes in the calendar. Updates thefilteredResources
state with the selected resources. -
const handleModalClose = () => { setIsModalOpen(false); };
: Handler to close the event creation modal. SetsisModalOpen
tofalse
. -
const myEmailAdapter = { ... }
: Email Adapter! An object that implements theEmailAdapter
interface expected byreact-agendfy
to send email notifications. We'll detail the implementation in the next step. For now, it only has aconsole.log
to demonstrate sending. -
const handleFormSubmit = (values, { resetForm }) => { ... }
: Handler for the Formik form'sonSubmit
event. Called when the user submits the event creation form. We'll detail the implementation in the next step. -
const FormFields = () => { ... }
: Auxiliary functional component (FormFields
) to render the "Start" and "End" date and time fields in the form. We will use theuseFormikContext
hook inside it to access Formik values. We'll detail this in the next step.
Step 5: Implementing Key Functions - EmailAdapter
, handleFormSubmit
, FormFields
, and JSX
Now let's detail the most important functions and build the JSX structure of our App
component.
Implementation of updateEvents
:
const updateEvents = (updatedEventOrEvents) => {
setEvents((prevEvents) => {
const updatedEventsArray = Array.isArray(updatedEventOrEvents)
? updatedEventOrEvents
: [updatedEventOrEvents];
const newEvents = prevEvents.map((event) => {
const updatedEvent = updatedEventsArray.find((e) => e.id === event.id);
return updatedEvent ? updatedEvent : event;
});
// Add new events that did not exist before
updatedEventsArray.forEach((updatedEvent) => {
if (!newEvents.find((event) => event.id === updatedEvent.id)) {
newEvents.push(updatedEvent);
}
});
return newEvents;
});
};
Explaining updateEvents
:
- This function is responsible for efficiently updating the
events
state when one or more events are changed (updated, resized, etc.). -
updatedEventOrEvents
: The argument can be a single event object or an array of event objects. -
setEvents((prevEvents) => { ... })
: We use the functional form ofsetEvents
to ensure we are working with the latest value of theprevEvents
state. - The logic within
setEvents
does the following:- Converts
updatedEventOrEvents
to an array (updatedEventsArray
) if it's a single event. - Uses
prevEvents.map(...)
to iterate over the existing events (prevEvents
). For each existing event:- Searches if there is a corresponding event in
updatedEventsArray
(with the sameid
). - If found, uses the updated event (
updatedEvent
). - If not found, keeps the existing event (
event
).
- Searches if there is a corresponding event in
- After updating existing events, iterates over
updatedEventsArray
again. For each updated event:- Checks if it already exists in
newEvents
(the array we are building). - If it doesn't exist (i.e., it's a new event), adds it to
newEvents
.
- Checks if it already exists in
- Returns
newEvents
, which is the new updated event array, so thatsetEvents
updates the state.
- Converts
Implementation of myEmailAdapter
(Email Adapter):
const myEmailAdapter = {
sendEmail: async (subject, body, recipient) => {
console.log("Sending email:", { subject, body, recipient });
// *** YOUR REAL EMAIL SENDING LOGIC GOES HERE ***
// Ex: Call an email service API, use Nodemailer, etc.
return Promise.resolve(); // Returns a resolved Promise (simulating successful sending)
},
};
Explaining myEmailAdapter
:
-
This object (
myEmailAdapter
) is what connectsreact-agendfy
to your email sending system. It needs to conform to theEmailAdapter
interface defined byreact-agendfy
:
interface EmailAdapter { sendEmail: (subject: string, body: string, recipient: string) => Promise<void>; }
-
sendEmail
: The only method required by theEmailAdapter
interface.-
subject
: Email subject (string).react-agendfy
generates the subject automatically. -
body
: Email body (string).react-agendfy
also generates the email body, including event details. -
recipient
: Recipient's email address (string). -
async
:sendEmail
must be an asynchronous function (async
) and return aPromise<void>
. This allowsreact-agendfy
to wait for the email sending to complete (or fail).
-
-
Real Implementation: In the example, the
sendEmail
implementation is very simple: just aconsole.log
to demonstrate that the function would be called. In a real application, you need to replaceconsole.log
with your real email sending logic. This may involve:- Calling an email service API (e.g., SendGrid, Mailgun, AWS SES, etc.) using
fetch
oraxios
. - Using a library like Nodemailer (if you are running a Node.js backend).
- Any other way your application uses to send emails.
- Calling an email service API (e.g., SendGrid, Mailgun, AWS SES, etc.) using
return Promise.resolve();
: In the example, we returnPromise.resolve()
to simulate that email sending is always successful. In a real implementation, you should handle errors and returnPromise.reject()
in case of sending failure.
Implementation of handleFormSubmit
(Form Submission):
const handleFormSubmit = (values, { resetForm }) => {
const newEvent = {
id: String(Date.now()),
title: values.title,
start: new Date(values.start).toISOString(),
end: new Date(values.end).toISOString(),
color: values.color,
isAllDay: false,
isMultiDay: false,
alertBefore: parseInt(values.alertBefore),
resources: values.resources,
};
console.log(newEvent) // For debug, see the created event in the console
setEvents((prevEvents) => [...prevEvents, newEvent]); // Add the new event to the state
setIsModalOpen(false); // Close the modal
resetForm(); // Clear the form
};
Explaining handleFormSubmit
:
- This function is called when the event creation form is submitted (when the user clicks "Create Event").
-
values
: Object containing the form field values (title, start date, end date, color, resources, alert, etc.), managed by Formik. -
{ resetForm }
: Object with auxiliary Formik actions. We useresetForm()
to clear the form fields after submission. -
Creation of the
newEvent
object:-
id: String(Date.now())
: Generates a unique ID for the new event using the current timestamp (in milliseconds) and converting it to a string. -
title: values.title, ... color: values.color
: Copies the values of the form fields to the corresponding properties of thenewEvent
object. -
start: new Date(values.start).toISOString()
: Converts the form start date and time (which comes as a string) to aDate
object and then to ISO 8601 UTC format usingtoISOString()
. Crucial for compatibility withreact-agendfy
and time zones. -
end: new Date(values.end).toISOString()
: Does the same for the end date and time. -
isAllDay: false, isMultiDay: false
: Sets tofalse
by default (these fields are not in the form in this example, but could be added). -
alertBefore: parseInt(values.alertBefore)
: Gets the "Alert Before" value from the form and converts it to an integer (parseInt
). This value will be used byreact-agendfy
to schedule notifications for this event. -
resources: values.resources
: Gets the array of resources selected in the form.
-
-
console.log(newEvent)
: (Optional) For debugging, logs the created event object to the browser console. -
setEvents((prevEvents) => [...prevEvents, newEvent])
: Updates theevents
state by adding thenewEvent
to the existing event array. We use the spread syntax (...prevEvents
) to create a new array with the previous events and the new event. -
setIsModalOpen(false)
: Closes the event creation modal. -
resetForm()
: Clears the form fields so the user can easily create another event.
Implementation of FormFields
Component (Date/Time Fields):
const FormFields = () => {
const { values, setFieldValue } = useFormikContext();
useEffect(() => {
if (selectedSlot && !values.start) {
setFieldValue("start", selectedSlot);
setFieldValue(
"end",
new Date(new Date(selectedSlot).getTime() + 60 * 60000).toISOString()
);
}
}, [selectedSlot, setFieldValue, values.start]);
return (
<>
<label
htmlFor="start"
className="block text-sm font-medium text-gray-700"
>
Start
</label>
<Field
name="start"
type="datetime-local"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
<ErrorMessage
name="start"
component="div"
className="text-red-500 text-sm mt-1"
/>
<label
htmlFor="end"
className="block text-sm font-medium text-gray-700"
>
End
</label>
<Field
name="end"
type="datetime-local"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
<ErrorMessage
name="end"
component="div"
className="text-red-500 text-sm mt-1"
/>
</>
);
};
Explaining FormFields
:
- This component is an auxiliary component to render the "Start" and "End" fields of the event form. We created a separate component to better organize the code and reuse the date auto-filling logic.
-
const { values, setFieldValue } = useFormikContext()
: Uses theuseFormikContext
hook from Formik to access the Formik context (form values and functions to manipulate them). This allowsFormFields
to interact with the parent Formik form. -
useEffect(() => { ... }, [selectedSlot, setFieldValue, values.start])
: AuseEffect
that executes the following logic whenselectedSlot
(the slot clicked on the calendar),setFieldValue
, orvalues.start
change:-
if (selectedSlot && !values.start)
: Checks if aselectedSlot
has been set (i.e., the user clicked on a slot) and if the "start" field of the form has not yet been filled (!values.start
). - If both conditions are true, it means the user has just clicked on a slot and the form is being opened to create a new event from that slot. Then:
-
setFieldValue("start", selectedSlot)
: Fills the "start" field of the form with theselectedSlot
(the clicked date and time). -
setFieldValue("end", new Date(new Date(selectedSlot).getTime() + 60 * 60000).toISOString())
: Fills the "end" field with a time 1 hour afterselectedSlot
.60 * 60000
milliseconds = 1 hour. Converts to ISO 8601 UTC withtoISOString()
.
-
-
-
return (<> ... </>)
: Returns JSX that renders:- Labels for "Start" and "End".
-
Field
components from Formik for the "start" and "end" fields. Important:type="datetime-local"
for date and time fields. -
ErrorMessage
components from Formik to display validation error messages for the "start" and "end" fields.
JSX Structure of the App
Component (rendering):
return (
<div className="flex">
{/* Calendar Column (half width) */}
<div className="w-1/2 p-4">
<Calendar
config={config}
events={events}
onEventUpdate={handleEventUpdate}
onEventResize={handleEventResizeUpdate}
onSlotClick={handleSlotClick}
resources={resources}
emailAdapter={myEmailAdapter}
filteredResources={filteredResources}
onResourceFilterChange={handleResourceFilterChange}
/>
</div>
{/* Form Column (half width) */}
<div className="w-1/2 p-4">
<div className="bg-white p-6 rounded-lg shadow-xl w-full max-w-md">
<h2 className="text-lg font-bold mb-4">Create Event</h2>
<Formik
initialValues={{ /* ... form initial values ... */ }}
validationSchema={/* ... Yup validation schema ... */ }
onSubmit={handleFormSubmit}
>
{({ values, handleChange }) => (
<Form>
{/* Form fields */}
<label className="block text-sm font-medium text-gray-700">Title</label>
<Field name="title" type="text" className="/* ... styles ... */" />
<ErrorMessage name="title" component="div" className="text-red-500 text-sm mt-1" />
<FormFields /> {/* Component for date/time fields */}
{/* Resources (checkboxes) */}
<label className="block text-sm font-medium text-gray-700">Resources</label>
<div className="mt-1 grid grid-cols-2 gap-2">
{resources.map((resource) => (
<label key={resource.id} className="flex items-center">
<Field type="checkbox" name="resources" value={resource.id} className="mr-2" />
{resource.name}
</label>
))}
</div>
{/* Duration (radio buttons) */}
<label className="block text-sm font-medium text-gray-700">Duration</label>
<div className="mt-1">
{/* ... radio buttons for "single", "multi", "recurring" ... */}
</div>
{/* Recurrence (text field, conditionally rendered) */}
{values.duration === "recurring" && (
<>
<label htmlFor="recurrence" className="block text-sm font-medium text-gray-700">Recurrence Rule (iCal RRule)</label>
<Field name="recurrence" type="text" className="/* ... styles ... */" />
</>
)}
{/* Alert Before (number input) */}
<label htmlFor="alertBefore" className="block text-sm font-medium text-gray-700">Alert Before (minutes)</label>
<Field name="alertBefore" type="number" className="/* ... styles ... */" />
{/* Color (color picker) */}
<label htmlFor="color" className="block text-sm font-medium text-gray-700">Color</label>
<Field name="color" type="color" className="/* ... styles ... */" />
<ErrorMessage name="color" component="div" className="text-red-500 text-sm mt-1" />
{/* "Cancel" and "Create Event" buttons */}
<div className="flex justify-end mt-4">
<button type="button" onClick={handleModalClose} className="/* ... "Cancel" button styles ... */">Cancel</button>
<button type="submit" className="/* ... "Create Event" button styles ... */">Create Event</button>
</div>
</Form>
)}
</Formik>
</div>
</div>
{/* Modal for the event form */}
<Modal
isOpen={isModalOpen}
onRequestClose={handleModalClose}
contentLabel="Create New Event"
>
{/* Modal content (form is already rendered above) */}
</Modal>
</div>
);
JSX Structure Explained:
-
<div className="flex">
: A containerdiv
withclassName="flex"
to create a two-column flexible layout (calendar and form side by side) using Tailwind CSS (classesw-1/2
,p-4
, etc. are Tailwind classes). -
Calendar Column (
<div className="w-1/2 p-4">
): Renders theCalendar
component fromreact-agendfy
.-
config={config}
: Passes the configuration object we defined. -
events={events}
: Passes the array of events from the state. -
onEventUpdate={handleEventUpdate}
: Passes the handler for event updates (drag & drop). -
onEventResize={handleEventResizeUpdate}
: Passes the handler for event resizing. -
onSlotClick={handleSlotClick}
: Passes the handler for clicking on empty slots (to open the creation modal). -
resources={resources}
: Passes the resources array. -
emailAdapter={myEmailAdapter}
: Passes our Email Adapter! This activates email notifications for events withalertBefore
defined. -
filteredResources={filteredResources}
: Passes the filtered resources state. -
onResourceFilterChange={handleResourceFilterChange}
: Passes the handler for resource filter changes.
-
-
Form Column (
<div className="w-1/2 p-4">
): Contains the event creation form, inside adiv
styled with Tailwind CSS (bg-white p-6 rounded-lg shadow-xl w-full max-w-md
).-
<Formik ...>
: Main Formik component that wraps the form.-
initialValues
: Defines the initial values of the form fields (empty title, empty dates, default color, no resources selected, duration "single", no recurrence, alert "0"). -
validationSchema
: Defines the validation schema using Yup. In the example, we validate thattitle
,start
, andend
are required fields. You can add more validations (e.g., start date before end date, RRule format, etc.). -
onSubmit={handleFormSubmit}
: Passes thehandleFormSubmit
handler to be called when the form is successfully submitted (after validation). -
{({ values, handleChange }) => (<Form> ... </Form>)}
: Renders the form itself. Receivesvalues
(field values) andhandleChange
(function to update values) from Formik as props.-
<Form>
: Formik component that renders the<form>
element. - Inside
<Form>
, we render the form fields:-
Title
:<label>
,<Field type="text" name="title" ... />
,<ErrorMessage name="title" ... />
. -
FormFields
: Our auxiliary component for the "Start" and "End" fields. -
Resources
:<label>
,<div grid grid-cols-2 gap-2>
, mapsresources
to checkboxes<Field type="checkbox" name="resources" ... />
. -
Duration
:<label>
,<div mt-1>
, radio buttons<Field type="radio" name="duration" ... />
for "Single day", "Multi-day", "Recurring". -
Recurrence
: Rendered conditionally ({values.duration === "recurring" && (...) }
) only if the "Recurring" duration is selected.<label>
,<Field type="text" name="recurrence" ... />
. -
Alert Before
:<label>
,<Field type="number" name="alertBefore" ... />
. -
Color
:<label>
,<Field type="color" name="color" ... />
,<ErrorMessage name="color" ... />
. - "Cancel" and "Create Event" buttons:
<div flex justify-end mt-4>
,<button type="button" onClick={handleModalClose} ...>Cancel</button>
,<button type="submit" ...>Create Event</button>
.
-
-
-
-
-
Modal (
<Modal ...>
):Modal
component fromreact-modal
to create the modal window.-
isOpen={isModalOpen}
: Controls whether the modal is open or closed, based on theisModalOpen
state. -
onRequestClose={handleModalClose}
: Handler called when the user tries to close the modal (e.g., clicking outside, pressing ESC). We callhandleModalClose
to close the modal. -
contentLabel="Create New Event"
: Label for modal accessibility. - The modal content is empty (
{/* Modal content ... */}
), because the Formik form is already rendered outside the modal, inside the form column. The modal only controls the visibility of the form.
-
Step 6: Complete Code to Copy and Paste
Here is the complete App.js
component code that we built step-by-step. You can copy and paste it directly into your React project to start experimenting!
import React, { useState, useEffect } from "react";
import { Calendar } from "react-agendfy";
import { Formik, Form, Field, ErrorMessage, useFormikContext } from "formik";
import * as Yup from "yup";
import Modal from "react-modal";
import "react-agendfy/dist/react-agendfy.css";
Modal.setAppElement("#root");
const resources = [
{ id: "r1", name: "Conference Room", type: "room" },
{ id: "r2", name: "John Doe", type: "person" },
{ id: "r3", name: "Projector", type: "equipment" },
{ id: "r4", name: "Holidays", type: "category" },
];
const initialEvents = [
{
id: "1",
title: "Planning Meeting",
start: "2025-03-01T09:00:00.000Z",
end: "2025-03-01T10:00:00.000Z",
color: "#3490dc",
isAllDay: false,
isMultiDay: false,
resources: [{ id: "r1", name: "Conference Room", type: "room" }],
},
{
id: "10",
title: "National Holiday",
start: "2025-03-02T06:00:00.000Z",
end: "2025-03-02T21:59:59.000Z",
color: "#ff9800",
isAllDay: false,
isMultiDay: true,
resources: [{ id: "r4", name: "Holidays", type: "category" }],
},
{
id: "4",
title: "Daily Standup",
start: "2025-03-06T04:00:00.000-03:00",
end: "2025-03-06T06:15:00.000-03:00",
color: "#4caf50",
recurrence: "FREQ=WEEKLY;INTERVAL=1;COUNT=10",
isAllDay: false,
isMultiDay: false,
resources: [],
},
{
id: "5",
title: "Carnival",
start: "2025-03-01T10:00:00.000Z",
end: "2025-03-02T12:15:00.000Z",
color: "#a10861",
isAllDay: true,
isMultiDay: true,
},
];
const config = {
timeZone: 'Africa/Lagos',
defaultView: "day",
slotDuration: 30,
slotLabelFormat: "HH:mm",
slotMin: "08:00",
slotMax: "24:00",
lang: "en",
today: "Today",
monthView: "Month",
weekView: "Week",
dayView: "Day",
listView: "List",
filter_resources: "Filter Resources",
clear_filter: "Clear Filter",
businessHours: {
enabled: true,
intervals: [
{ daysOfWeek: [1, 2, 3, 4, 5], startTime: "09:00", endTime: "17:00" }
]
},
alerts: {
enabled: true,
thresholdMinutes: 15,
},
};
function App() {
const [events, setEvents] = useState(() => {
const storedEvents = localStorage.getItem("calendarEvents_dev");
return storedEvents ? JSON.parse(storedEvents) : initialEvents;
});
const [filteredResources, setFilteredResources] = useState([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedSlot, setSelectedSlot] = useState(null);
useEffect(() => {
localStorage.setItem("calendarEvents_dev", JSON.stringify(events));
}, [events]);
const updateEvents = (updatedEventOrEvents) => {
setEvents((prevEvents) => {
const updatedEventsArray = Array.isArray(updatedEventOrEvents)
? updatedEventOrEvents
: [updatedEventOrEvents];
const newEvents = prevEvents.map((event) => {
const updatedEvent = updatedEventsArray.find((e) => e.id === event.id);
return updatedEvent ? updatedEvent : event;
});
// Add new events that did not exist before
updatedEventsArray.forEach((updatedEvent) => {
if (!newEvents.find((event) => event.id === updatedEvent.id)) {
newEvents.push(updatedEvent);
}
});
return newEvents;
});
};
const handleEventUpdate = (updatedEventOrEvents) => {
updateEvents(updatedEventOrEvents);
};
const handleEventResizeUpdate = (updatedEventOrEvents) => {
updateEvents(updatedEventOrEvents);
};
const handleSlotClick = (slotTime) => {
setSelectedSlot(slotTime);
setIsModalOpen(true);
};
const handleResourceFilterChange = (selectedResources) => {
setFilteredResources(selectedResources);
};
const handleModalClose = () => {
setIsModalOpen(false);
};
const myEmailAdapter = {
sendEmail: async (subject, body, recipient) => {
console.log("Sending email:", { subject, body, recipient });
// *** YOUR REAL EMAIL SENDING LOGIC GOES HERE ***
// Ex: Call an email service API, use Nodemailer, etc.
return Promise.resolve();
},
};
const handleFormSubmit = (values, { resetForm }) => {
const newEvent = {
id: String(Date.now()),
title: values.title,
start: new Date(values.start).toISOString(),
end: new Date(values.end).toISOString(),
color: values.color,
isAllDay: false,
isMultiDay: false,
alertBefore: parseInt(values.alertBefore),
resources: values.resources,
};
console.log(newEvent)
setEvents((prevEvents) => [...prevEvents, newEvent]);
setIsModalOpen(false);
resetForm();
};
const FormFields = () => {
const { values, setFieldValue } = useFormikContext();
useEffect(() => {
if (selectedSlot && !values.start) {
setFieldValue("start", selectedSlot);
setFieldValue(
"end",
new Date(new Date(selectedSlot).getTime() + 60 * 60000).toISOString()
);
}
}, [selectedSlot, setFieldValue, values.start]);
return (
<>
<label
htmlFor="start"
className="block text-sm font-medium text-gray-700"
>
Start
</label>
<Field
name="start"
type="datetime-local"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
<ErrorMessage
name="start"
component="div"
className="text-red-500 text-sm mt-1"
/>
<label
htmlFor="end"
className="block text-sm font-medium text-gray-700"
>
End
</label>
<Field
name="end"
type="datetime-local"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
<ErrorMessage
name="end"
component="div"
className="text-red-500 text-sm mt-1"
/>
</>
);
};
return (
<div className="flex">
{/* Calendar Column */}
<div className="w-1/2 p-4">
<Calendar
config={config}
events={events}
onEventUpdate={handleEventUpdate}
onEventResize={handleEventResizeUpdate}
onSlotClick={handleSlotClick}
resources={resources}
emailAdapter={myEmailAdapter}
filteredResources={filteredResources}
onResourceFilterChange={handleResourceFilterChange}
/>
</div>
{/* Form Column */}
<div className="w-1/2 p-4">
<div className="bg-white p-6 rounded-lg shadow-xl w-full max-w-md">
<h2 className="text-lg font-bold mb-4">Create Event</h2>
<Formik
initialValues={{
title: "",
start: "",
end: "",
color: "#3490dc",
resources: [],
duration: "single", // single, multi, recurring
recurrence: "", // For recurring events
alertBefore: 0,
}}
validationSchema={Yup.object({
title: Yup.string().required("Required"),
start: Yup.date().required("Required"),
end: Yup.date().required("Required"),
})}
onSubmit={handleFormSubmit}
>
{({ values, handleChange }) => (
<Form>
<label
htmlFor="title"
className="block text-sm font-medium text-gray-700"
>
Title
</label>
<Field
name="title"
type="text"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
<ErrorMessage
name="title"
component="div"
className="text-red-500 text-sm mt-1"
/>
<FormFields />
{/* Resources */}
<label className="block text-sm font-medium text-gray-700">
Resources
</label>
<div className="mt-1 grid grid-cols-2 gap-2">
{resources.map((resource) => (
<label key={resource.id} className="flex items-center">
<Field
type="checkbox"
name="resources"
value={resource.id}
className="mr-2"
/>
{resource.name}
</label>
))}
</div>
{/* Duration */}
<label className="block text-sm font-medium text-gray-700">
Duration
</label>
<div className="mt-1">
<label className="mr-4">
<Field type="radio" name="duration" value="single" />
Single day
</label>
<label className="mr-4">
<Field type="radio" name="duration" value="multi" />
Multi-day
</label>
<label>
<Field type="radio" name="duration" value="recurring" />
Recurring
</label>
</div>
{/* Recurrence (if selected) */}
{values.duration === "recurring" && (
<>
<label
htmlFor="recurrence"
className="block text-sm font-medium text-gray-700"
>
Recurrence Rule (iCal RRule)
</label>
<Field
name="recurrence"
type="text"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
</>
)}
{/* Alert Before */}
<label
htmlFor="alertBefore"
className="block text-sm font-medium text-gray-700"
>
Alert Before (minutes)
</label>
<Field
name="alertBefore"
type="number"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
<label
htmlFor="color"
className="block text-sm font-medium text-gray-700"
>
Color
</label>
<Field
name="color"
type="color"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
<ErrorMessage
name="color"
component="div"
className="text-red-500 text-sm mt-1"
/>
<div className="flex justify-end mt-4">
<button
type="button"
onClick={handleModalClose}
className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded mr-2"
>
Cancel
</button>
<button
type="submit"
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded"
>
Create Event
</button>
</div>
</Form>
)}
</Formik>
</div>
</div>
{/* Modal for the event form */}
<Modal
isOpen={isModalOpen}
onRequestClose={handleModalClose}
contentLabel="Create New Event"
/>
</div>
);
}
export default App;
Step 7: Running the Application and Next Steps
With all the code implemented, you can now run your React application and see the calendar in action!
- Start your React server:
npm start
oryarn start
in the terminal. - Open the browser: Access the address that React shows in the terminal (usually
http://localhost:3000
).
You should see the calendar rendered, with business hours highlighted (week and day views) and ready to add new events!
Next Steps and Improvements:
-
Implement Real Email Sending: Replace the
console.log
inmyEmailAdapter.sendEmail
with your application's email sending logic (API, Nodemailer, etc.). -
Backend for Notifications:
react-agendfy
handles the interface and configuration of notifications, but not the backend logic to periodically check events and trigger emails. You will need to implement a backend (e.g., Node.js, Python, etc.) that:- Reads events from your database.
- Checks which events have
alertBefore
defined and are approaching their start time. - Calls the
sendEmail
method of theEmailAdapter
(which you passed toCalendar
in the frontend) to send emails. - Schedule this periodic check (e.g., using cron jobs, serverless functions, etc.).
-
Data Persistence in a Database: In the example, we are using
localStorage
for simple browser persistence. For a real application, you will need to connect to a database (e.g., PostgreSQL, MySQL, MongoDB, etc.) to store and retrieve events robustly. - More Form Customization: Add more fields to the form to customize events (e.g., description, more user-friendly recurrence options, options for all-day/multi-day events, email recipient selection for notifications, etc.).
- Improve the Interface: Use more elaborate UI components (e.g., component libraries like Material UI, Chakra UI, Ant Design, Tailwind UI) to style the calendar and form, making the interface more professional and user-friendly.
- Testing: Write unit and integration tests to ensure the quality and stability of your calendar component.
Conclusion: react-agendfy
- React Scheduling Done Right
react-agendfy
is a powerful and flexible React calendar component that goes beyond basic calendar views and offers essential features for professional scheduling applications, such as customizable business hours and email notifications. With this tutorial, you have a solid starting point to build amazing scheduling systems with React!
Notes:
- Styling: The code uses Tailwind CSS classes for basic styling. For a more elaborate look, you can customize Tailwind classes, use custom CSS, or integrate a UI component library.
- Email Backend: Remember that real email sending requires a backend. The
EmailAdapter
in the frontend only initiates the sending process, but the sending logic itself and the scheduling of notifications must be implemented in the backend. - Adaptability: Adapt this tutorial and code for the specific needs of your project.
react-agendfy
offers many customization options, explore the official documentation to go deeper!
I hope this complete tutorial is helpful! If you have more questions or need help with any specific point, let me know!
Top comments (0)