DEV Community

Cover image for Smart Scheduling with React: Master Notifications and Business Hours in `react-agendfy`
Marcio Zebedeu
Marcio Zebedeu

Posted on

Smart Scheduling with React: Master Notifications and Business Hours in `react-agendfy`

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
Enter fullscreen mode Exit fullscreen mode

What did we install and why?

  • react-agendfy: The heart of our calendar, the main component.
  • formik and yup: 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");
Enter fullscreen mode Exit fullscreen mode

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 the Calendar component from react-agendfy. This is the component that will render the calendar in our application.
  • { Formik, Form, Field, ErrorMessage, useFormikContext } from "formik": Imports from formik, 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 in FormFields).
  • * as Yup from "yup": Imports the yup library, which we will use to define validation rules for our form (e.g., ensuring required fields are filled).
  • Modal from "react-modal": Imports the Modal component from react-modal, to create modal windows (pop-ups) for adding new events to the calendar.
  • "react-agendfy/dist/react-agendfy.css": Crucial! Imports the react-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,
  },
];
Enter fullscreen mode Exit fullscreen mode

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 as resources, with id, name, and type.
  • 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 use alertBefore in initialEvents, 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,
  },
};
Enter fullscreen mode Exit fullscreen mode

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. If false, 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. If false, the alertBefore 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 customize alertBefore individually in each event object (as we already saw in initialEvents).

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 use useState to create this state and setEvents to update it.
    • The initial value of events is obtained from localStorage (if there are events saved there, for persistence between sessions) or from initialEvents (if there is nothing in localStorage or the first time the application runs).
  • 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. Initially null.

Side Effect (useEffect):

  • useEffect(() => { localStorage.setItem("calendarEvents_dev", JSON.stringify(events)); }, [events]);: This useEffect ensures that events are saved in localStorage whenever the events 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 the events state with one or more updated events. It receives an event or an array of events, and updates the events array in the state, keeping events that have not been changed and adding new events if needed.
  • const handleEventUpdate = (updatedEventOrEvents) => { updateEvents(updatedEventOrEvents); };: Handler for the Calendar's onEventUpdate event. Called when an event is moved (drag and drop) or resized on the calendar. It simply forwards the updated event to updateEvents to update the state.
  • const handleEventResizeUpdate = (updatedEventOrEvents) => { ... }: Handler for the Calendar's onEventResize event. Called when an event is resized (by dragging the border). Similar to handleEventUpdate, it updates the state with updateEvents.
  • const handleSlotClick = (slotTime) => { setSelectedSlot(slotTime); setIsModalOpen(true); };: Handler for the Calendar's onSlotClick event. Called when the user clicks on an empty slot on the calendar. Sets the selectedSlot to the clicked time and opens the event creation modal (setIsModalOpen(true)).
  • const handleResourceFilterChange = (selectedResources) => { setFilteredResources(selectedResources); };: Handler for the Calendar's onResourceFilterChange event. Called when the resource filter changes in the calendar. Updates the filteredResources state with the selected resources.
  • const handleModalClose = () => { setIsModalOpen(false); };: Handler to close the event creation modal. Sets isModalOpen to false.
  • const myEmailAdapter = { ... }: Email Adapter! An object that implements the EmailAdapter interface expected by react-agendfy to send email notifications. We'll detail the implementation in the next step. For now, it only has a console.log to demonstrate sending.
  • const handleFormSubmit = (values, { resetForm }) => { ... }: Handler for the Formik form's onSubmit 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 the useFormikContext 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;
    });
  };
Enter fullscreen mode Exit fullscreen mode

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 of setEvents to ensure we are working with the latest value of the prevEvents state.
  • The logic within setEvents does the following:
    1. Converts updatedEventOrEvents to an array (updatedEventsArray) if it's a single event.
    2. 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 same id).
      • If found, uses the updated event (updatedEvent).
      • If not found, keeps the existing event (event).
    3. 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.
    4. Returns newEvents, which is the new updated event array, so that setEvents updates the state.

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)
    },
  };
Enter fullscreen mode Exit fullscreen mode

Explaining myEmailAdapter:

  • This object (myEmailAdapter) is what connects react-agendfy to your email sending system. It needs to conform to the EmailAdapter interface defined by react-agendfy:

    interface EmailAdapter {
      sendEmail: (subject: string, body: string, recipient: string) => Promise<void>;
    }
    
  • sendEmail: The only method required by the EmailAdapter 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 a Promise<void>. This allows react-agendfy to wait for the email sending to complete (or fail).
  • Real Implementation: In the example, the sendEmail implementation is very simple: just a console.log to demonstrate that the function would be called. In a real application, you need to replace console.log with your real email sending logic. This may involve:

    • Calling an email service API (e.g., SendGrid, Mailgun, AWS SES, etc.) using fetch or axios.
    • Using a library like Nodemailer (if you are running a Node.js backend).
    • Any other way your application uses to send emails.
  • return Promise.resolve();: In the example, we return Promise.resolve() to simulate that email sending is always successful. In a real implementation, you should handle errors and return Promise.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
  };
Enter fullscreen mode Exit fullscreen mode

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 use resetForm() 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 the newEvent object.
    • start: new Date(values.start).toISOString(): Converts the form start date and time (which comes as a string) to a Date object and then to ISO 8601 UTC format using toISOString(). Crucial for compatibility with react-agendfy and time zones.
    • end: new Date(values.end).toISOString(): Does the same for the end date and time.
    • isAllDay: false, isMultiDay: false: Sets to false 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 by react-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 the events state by adding the newEvent 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"
        />
      </>
    );
  };
Enter fullscreen mode Exit fullscreen mode

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 the useFormikContext hook from Formik to access the Formik context (form values and functions to manipulate them). This allows FormFields to interact with the parent Formik form.
  • useEffect(() => { ... }, [selectedSlot, setFieldValue, values.start]): A useEffect that executes the following logic when selectedSlot (the slot clicked on the calendar), setFieldValue, or values.start change:
    • if (selectedSlot && !values.start): Checks if a selectedSlot 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 the selectedSlot (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 after selectedSlot. 60 * 60000 milliseconds = 1 hour. Converts to ISO 8601 UTC with toISOString().
  • 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>
  );
Enter fullscreen mode Exit fullscreen mode

JSX Structure Explained:

  • <div className="flex">: A container div with className="flex" to create a two-column flexible layout (calendar and form side by side) using Tailwind CSS (classes w-1/2, p-4, etc. are Tailwind classes).
  • Calendar Column (<div className="w-1/2 p-4">): Renders the Calendar component from react-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 with alertBefore 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 a div 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 that title, start, and end are required fields. You can add more validations (e.g., start date before end date, RRule format, etc.).
      • onSubmit={handleFormSubmit}: Passes the handleFormSubmit handler to be called when the form is successfully submitted (after validation).
      • {({ values, handleChange }) => (<Form> ... </Form>)}: Renders the form itself. Receives values (field values) and handleChange (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>, maps resources 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 from react-modal to create the modal window.
    • isOpen={isModalOpen}: Controls whether the modal is open or closed, based on the isModalOpen state.
    • onRequestClose={handleModalClose}: Handler called when the user tries to close the modal (e.g., clicking outside, pressing ESC). We call handleModalClose 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;
Enter fullscreen mode Exit fullscreen mode

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!

  1. Start your React server: npm start or yarn start in the terminal.
  2. 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 in myEmailAdapter.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 the EmailAdapter (which you passed to Calendar 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)