reactjsreact-big-calendar

react big calendar - dynamically update backgroundEvents


i'm Trying to display the opening hours of a business in a react big calendar as backgroundEvents. The user should be able to add "special" opening hours, which should override the "normal" opening hours.

Heres an example of the data:

  1. "Normal" opening hours (0 = monday):
const openingHours = {
  0: [
    { open: "04:00", close: "12:00" },
    { open: "13:00", close: "17:45" },
  ],
  1: [{ open: "05:00", close: "17:45" }],
  2: [{ open: "06:00", close: "17:45" }],
  3: [{ open: "08:00", close: "17:45" }],
  4: [
    { open: "06:30", close: "12:00" },
    { open: "13:30", close: "23:00" },
  ],
  5: [{ open: "08:00", close: "17:45" }],
  6: [],
};
  1. "Special" opening hours:
const specialOpeningHours = [
  { open: "2024-01-05T05:00:00", close: "2024-01-05T12:00:00" },
  { open: "2024-01-05T13:00:00", close: "2024-01-05T23:59:00" },
];

i'm trying to update my Calendar when the onRangeChange hook is fired:

      <DnDCalendar
        localizer={localizer}
        events={myEventsList}
        startAccessor="start"
        endAccessor="end"
        backgroundEvents={backgroundEvents}
        onRangeChange={handleRangeChange}
      />

this is my corresponding function:

  const handleRangeChange = (range) => {
    const dateRange = Array.isArray(range) ? range : [range.start, range.end];
    console.log("Date Range:", dateRange);
    const newBackgroundEvents = getBackgroundEvents(dateRange);
    setBackgroundEvents(newBackgroundEvents);
  };

  const getBackgroundEvents = (range) => {
    let events = [];

    for (
      let date = new Date(range[0]);
      date <= range[1];
      date.setDate(date.getDate() + 1)
    ) {
      // Adjusting so that Monday = 0, Tuesday = 1, ..., Sunday = 6
      const day = (date.getDay() + 6) % 7;

      // Find if special hours apply to this date
      const formattedDate = formatISO(date, { representation: "date" });
      const specialHours = specialOpeningHours.filter((special) =>
        special.open.startsWith(formattedDate)
      );

      // Determine hours to use (special or normal)
      let hours = specialHours.length > 0 ? specialHours : openingHours[day];

      // Convert hours to background events
      hours.forEach((slot) => {
        let start, end;
        if (specialHours.length > 0) {
          start = parseISO(slot.open);
          end = parseISO(slot.close);
        } else {
          start = parseISO(`${formattedDate}T${slot.open}`);
          end = parseISO(`${formattedDate}T${slot.close}`);
        }
        events.push({ start, end });
      });
    }

    return events;
  };

When running this function, the calendar does not update to the next or previous week but stays at the week i'm currently on.

i tried adding the "new" BackgroundEvents manually:

  const handleRangeChange = (range) => {
    const dateRange = Array.isArray(range) ? range : [range.start, range.end];
    console.log("Date Range:", dateRange); // Add this line for debugging
    const newBackgroundEvents = getBackgroundEvents(dateRange);
    /*HERE*/
    setBackgroundEvents([
      {
        start: parseISO("2024-01-07T13:05:00"),
        end: parseISO("2024-01-07T14:00:00"),
      },
    ]);
    /*HERE*/
  };

but this did not work. how can i update the backgroundEvents onRangeChange?

this is my complete code:

import "../../../styles/components/scheduler.scss";

import React, { useEffect, useState, useCallback, useRef } from "react";
import {
  getISOWeek,
  addYears,
  subYears,
  parseISO,
  isSameDay,
  formatISO,
} from "date-fns";

import {
  Calendar,
  dateFnsLocalizer,
  Views,
  CalendarProps,
} from "react-big-calendar";
import format from "date-fns/format";
import parse from "date-fns/parse";
import startOfWeek from "date-fns/startOfWeek";
import endOfWeek from "date-fns/endOfWeek";
import getDay from "date-fns/getDay";
import de from "date-fns/locale/de";
import withDragAndDrop from "react-big-calendar/lib/addons/dragAndDrop/withDragAndDrop";

const openingHours = {
  0: [
    { open: "04:00", close: "12:00" },
    { open: "13:00", close: "17:45" },
  ],
  1: [{ open: "05:00", close: "17:45" }],
  2: [{ open: "06:00", close: "17:45" }],
  3: [{ open: "08:00", close: "17:45" }],
  4: [
    { open: "06:30", close: "12:00" },
    { open: "13:30", close: "23:00" },
  ],
  5: [{ open: "08:00", close: "17:45" }],
  6: [],
};
const specialOpeningHours = [
  { open: "2024-01-05T05:00:00", close: "2024-01-05T12:00:00" },
  { open: "2024-01-05T13:00:00", close: "2024-01-05T23:59:00" },
];

const myEventsList = [
  {
    title: "My Event",
    start: parseISO("2024-01-07T13:05:00"),
    end: parseISO("2024-01-07T14:00:00"),
  },
  {
    title: "My Event 2",
    start: parseISO("2024-01-07T13:35:00"),
    end: parseISO("2024-01-07T14:30:00"),
  },
  {
    title: "My Event 3",
    start: parseISO("2024-01-12T13:35:00"),
    end: parseISO("2024-01-12T14:30:00"),
  },
];

function Scheduler() {
  const [backgroundEvents, setBackgroundEvents] = useState([]);
  const clickRef = useRef(null);

  const getCurrentWeekRange = () => {
    const today = new Date();
    const start = startOfWeek(today, { weekStartsOn: 0 }); // Adjust weekStartsOn based on your locale
    const end = endOfWeek(today, { weekStartsOn: 0 });
    return [start, end];
  };

  useEffect(() => {
    const currentWeekRange = getCurrentWeekRange();
    handleRangeChange(currentWeekRange);
  }, []);
  useEffect(() => {
    /* This is to prevent a memory leak, in the off chance that you
      teardown your interface prior to the timed method being called.*/
    return () => {
      window.clearTimeout(clickRef?.current);
    };
  }, []);

  const DnDCalendar = withDragAndDrop(Calendar);
  const locales = {
    de: de,
  };

  const messages = {
    week: "Woche",
    work_week: "Arbeitswoche",
    day: "Tag",
    month: "Monat",
    previous: "zurück",
    next: "vor",
    today: "Heute",
    agenda: "Liste",
    showMore: (total) => `+${total} más`,
  };

  const localizer = dateFnsLocalizer({
    format,
    parse,
    startOfWeek,
    getDay,
    locales,
  });

  function updateEvent(event) {
    console.log(event);
    console.log(event.start.toUTCString());
  }

  function newEvent(event) {
    console.log(event);
  }

  const onSelectEvent = useCallback((calEvent) => {
    console.log("onSelectEvent");
    window.clearTimeout(clickRef?.current);
    clickRef.current = window.setTimeout(() => {
      console.log(calEvent);
    }, 250);
  }, []);

  const onDoubleClickEvent = useCallback((calEvent) => {
    console.log("onDoubleClick");
    window.clearTimeout(clickRef?.current);
    clickRef.current = window.setTimeout(() => {
      console.log(calEvent);
    }, 250);
  }, []);
  const handleRangeChange = (range) => {
    const dateRange = Array.isArray(range) ? range : [range.start, range.end];
    console.log("Date Range:", dateRange); // Add this line for debugging
    const newBackgroundEvents = getBackgroundEvents(dateRange);
    setBackgroundEvents([
      {
        start: parseISO("2024-01-07T13:05:00"),
        end: parseISO("2024-01-07T14:00:00"),
      },
    ]);
  };

  const getBackgroundEvents = (range) => {
    let events = [];

    for (
      let date = new Date(range[0]);
      date <= range[1];
      date.setDate(date.getDate() + 1)
    ) {
      // Adjusting so that Monday = 0, Tuesday = 1, ..., Sunday = 6
      const day = (date.getDay() + 6) % 7;

      // Find if special hours apply to this date
      const formattedDate = formatISO(date, { representation: "date" });
      const specialHours = specialOpeningHours.filter((special) =>
        special.open.startsWith(formattedDate)
      );

      // Determine hours to use (special or normal)
      let hours = specialHours.length > 0 ? specialHours : openingHours[day];

      // Convert hours to background events
      hours.forEach((slot) => {
        let start, end;
        if (specialHours.length > 0) {
          start = parseISO(slot.open);
          end = parseISO(slot.close);
        } else {
          start = parseISO(`${formattedDate}T${slot.open}`);
          end = parseISO(`${formattedDate}T${slot.close}`);
        }
        events.push({ start, end });
      });
    }

    return events;
  };

  return (
    <div className="w-full h-full">
      <DnDCalendar
        defaultView="week"
        selectable="true"
        localizer={localizer}
        culture="de"
        events={myEventsList}
        startAccessor="start"
        endAccessor="end"
        style={{ height: "100%" }}
        messages={messages}
        views={[Views.WEEK, Views.DAY]}
        dayLayoutAlgorithm={"no-overlap"}
        onEventDrop={updateEvent}
        onEventResize={updateEvent}
        onSelectEvent={onSelectEvent}
        onSelectSlot={newEvent}
        backgroundEvents={backgroundEvents}
        onDoubleClick={onDoubleClickEvent}
        onRangeChange={handleRangeChange}
      />
    </div>
  );
}

export default Scheduler;

Solution

  • Both the events and backgroundEvents are props. Updating the prop would update the calendar. I would also use controlled props for date and view. Do not forget that all dates, used by Big Calendar, should be true JS Date objects. This goes for your start/end for either type of 'event'.