sparkletown/sparkle

View on GitHub
src/components/organisms/NavBarSchedule/NavBarSchedule.tsx

Summary

Maintainability
C
7 hrs
Test Coverage
import React, { useCallback, useMemo, useState } from "react";
import classNames from "classnames";
import {
  addDays,
  format,
  fromUnixTime,
  isToday,
  startOfDay,
  startOfToday,
} from "date-fns";

import { ALWAYS_EMPTY_ARRAY, PLATFORM_BRAND_NAME } from "settings";

import { ScheduledEvent } from "types/venues";

import { createCalendar, downloadCalendar } from "utils/calendar";
import {
  eventTimeAndOrderComparator,
  isEventWithinDateAndNotFinished,
} from "utils/event";
import { range } from "utils/range";
import { formatDateRelativeToNow } from "utils/time";

import { useRelatedVenues } from "hooks/useRelatedVenues";
import { useShowHide } from "hooks/useShowHide";
import { useUser } from "hooks/useUser";
import useVenueScheduleEvents from "hooks/useVenueScheduleEvents";
import { useWorldParams } from "hooks/worlds/useWorldParams";

import { Breadcrumbs } from "components/molecules/Breadcrumbs";
import { Schedule } from "components/molecules/Schedule";

import { ButtonNG } from "components/atoms/ButtonNG";
import { Toggler } from "components/atoms/Toggler";

import { prepareForSchedule } from "./utils";

import "./NavBarSchedule.scss";

interface ScheduleDay {
  daysEvents: ScheduledEvent[];
  scheduleDate: Date;
}

const emptyPersonalizedSchedule = {};
interface NavBarScheduleProps {
  isVisible?: boolean;
  venueId: string;
}

export const NavBarSchedule: React.FC<NavBarScheduleProps> = ({
  isVisible,
  venueId,
}) => {
  const { currentVenue: venue, findVenueInRelatedVenues } = useRelatedVenues({
    currentVenueId: venueId,
  });

  const { worldSlug } = useWorldParams();

  const { userWithId } = useUser();
  const userEventIds =
    userWithId?.myPersonalizedSchedule ?? emptyPersonalizedSchedule;

  const [selectedDayIndex, setSelectedDayIndex] = useState(0);

  const {
    isShown: showPersonalisedSchedule,
    toggle: togglePersonalisedSchedule,
  } = useShowHide(false);

  const {
    dayDifference,
    firstScheduleDate,
    liveAndFutureEvents,
    isEventsLoading,
    sovereignVenue,
    relatedVenues,
  } = useVenueScheduleEvents({ userEventIds });

  const scheduledStartDate = sovereignVenue?.start_utc_seconds;

  const isNotSovereignVenue = venue?.id !== sovereignVenue?.id;

  const [filterRelatedEvents, setFilterRelatedEvents] = useState(false);

  // @debt: probably will need to be re-calculated based on minDateUtcSeconds instead of startOfDay.Check later
  const firstDayOfSchedule = useMemo(() => {
    return scheduledStartDate
      ? startOfDay(fromUnixTime(scheduledStartDate))
      : startOfToday();
  }, [scheduledStartDate]);

  const isScheduleTimeshifted = !isToday(firstDayOfSchedule);
  const hasSavedEvents = !!liveAndFutureEvents.filter((event) => event.isSaved)
    .length;

  const isShowPersonalDownloadBtn = hasSavedEvents && showPersonalisedSchedule;

  const weekdays = useMemo(() => {
    const formatDayLabel = (day: Date | number) => {
      if (isScheduleTimeshifted) {
        return format(day, "do");
      } else {
        return formatDateRelativeToNow(day, {
          formatOtherDate: (dateOrTimestamp) => format(dateOrTimestamp, "do"),
          formatTomorrow: (dateOrTimestamp) => format(dateOrTimestamp, "do"),
        });
      }
    };

    if (dayDifference <= 0) return ALWAYS_EMPTY_ARRAY;

    return range(dayDifference).map((dayIndex) => {
      const day = addDays(firstScheduleDate, dayIndex);

      const daysWithEvents = liveAndFutureEvents.some(
        isEventWithinDateAndNotFinished(day)
      );

      const classes = classNames("NavBarSchedule__weekday", {
        "NavBarSchedule__weekday--active": dayIndex === selectedDayIndex,
        "NavBarSchedule__weekday--disabled": !daysWithEvents,
      });

      const formattedDay = formatDayLabel(day);

      return (
        <li
          key={day.toISOString()}
          className={classes}
          onClick={() => {
            setSelectedDayIndex(dayIndex);
          }}
        >
          <button
            aria-label={formattedDay}
            className="NavBarSchedule__weekday-button"
          >
            {formattedDay}
          </button>
        </li>
      );
    });
  }, [
    selectedDayIndex,
    isScheduleTimeshifted,
    dayDifference,
    liveAndFutureEvents,
    firstScheduleDate,
  ]);

  const schedule: ScheduleDay = useMemo(() => {
    const day = addDays(firstScheduleDate, selectedDayIndex);

    const daysEvents = liveAndFutureEvents.filter(
      isEventWithinDateAndNotFinished(day)
    );

    const eventsFilledWithPriority = daysEvents.sort(
      eventTimeAndOrderComparator
    );

    const currentVenueBookMarkEvents = eventsFilledWithPriority.filter(
      ({ isSaved, spaceId: eventSpaceId }) =>
        isSaved && eventSpaceId?.toLowerCase() === venueId
    );

    const currentVenueEvents = eventsFilledWithPriority.filter(
      ({ spaceId: eventSpaceId }) => eventSpaceId?.toLowerCase() === venueId
    );

    const personalisedSchedule = filterRelatedEvents
      ? currentVenueBookMarkEvents
      : eventsFilledWithPriority.filter((event) => event.isSaved);

    return {
      scheduleDate: day,
      daysEvents: showPersonalisedSchedule
        ? personalisedSchedule
        : filterRelatedEvents
        ? currentVenueEvents
        : eventsFilledWithPriority,
    };
  }, [
    firstScheduleDate,
    selectedDayIndex,
    liveAndFutureEvents,
    filterRelatedEvents,
    showPersonalisedSchedule,
    venueId,
  ]);

  const scheduleWithAttendees = {
    ...schedule,
    daysEvents: schedule.daysEvents.map((event) => {
      const portalVenue = findVenueInRelatedVenues({ spaceId: event.spaceId });

      return prepareForSchedule({
        worldSlug,
        relatedVenues,
        usersEvents: userEventIds,
        recentRoomUsersCount: portalVenue?.recentUserCount,
      })(event);
    }),
  };
  const downloadPersonalEventsCalendar = useCallback(() => {
    const allPersonalEvents: ScheduledEvent[] = liveAndFutureEvents
      .map(
        prepareForSchedule({
          worldSlug,
          relatedVenues,
          usersEvents: userEventIds,
        })
      )
      .filter((event) => event.isSaved);

    downloadCalendar({
      calendar: createCalendar({
        worldSlug,
        relatedVenues,
        events: allPersonalEvents,
      }),
      calendarName: `${PLATFORM_BRAND_NAME}_Personal`,
    });
  }, [liveAndFutureEvents, relatedVenues, userEventIds, worldSlug]);

  const downloadAllEventsCalendar = useCallback(() => {
    downloadCalendar({
      calendar: createCalendar({
        worldSlug,
        relatedVenues,
        events: liveAndFutureEvents,
      }),
      calendarName: `${PLATFORM_BRAND_NAME}_Full`,
    });
  }, [worldSlug, relatedVenues, liveAndFutureEvents]);

  const containerClasses = classNames("NavBarSchedule", {
    "NavBarSchedule--show": isVisible,
  });

  const breadcrumbedLocations = useMemo(() => {
    if (!sovereignVenue) return [];

    const locations = [{ key: sovereignVenue.id, name: sovereignVenue.name }];

    if (venue && isNotSovereignVenue)
      locations.push({ key: venue.id, name: venue.name });

    return locations;
  }, [isNotSovereignVenue, sovereignVenue, venue]);

  const onBreacrumbsSelect = useCallback(
    (key: string) => {
      setFilterRelatedEvents(key === venue?.id && isNotSovereignVenue);
    },
    [venue, isNotSovereignVenue]
  );

  return (
    <div className={containerClasses}>
      <div className="NavBarSchedule__wrapper">
        <ul className="NavBarSchedule__weekdays">{weekdays}</ul>
        {venue && sovereignVenue && (
          <Breadcrumbs
            containerClassName="NavBarSchedule__breadcrumbs"
            label="Schedule for"
            onSelect={onBreacrumbsSelect}
            locations={breadcrumbedLocations}
          />
        )}
        <Toggler
          containerClassName="NavBarSchedule__bookmarked-toggle"
          name="bookmarked-toggle"
          toggled={showPersonalisedSchedule}
          onChange={togglePersonalisedSchedule}
          label="Bookmarked events"
        />
        <Schedule
          showPersonalisedSchedule={showPersonalisedSchedule}
          isLoading={isEventsLoading}
          {...scheduleWithAttendees}
        />
        {!isEventsLoading && (
          <div className="NavBarSchedule__download-buttons">
            {isShowPersonalDownloadBtn && (
              <ButtonNG
                onClick={downloadPersonalEventsCalendar}
                className="NavBarSchedule__download-schedule-button"
                variant="primary"
              >
                Download your schedule
              </ButtonNG>
            )}
            <ButtonNG
              onClick={downloadAllEventsCalendar}
              className="NavBarSchedule__download-schedule-button"
              variant="primary"
            >
              Download full schedule
            </ButtonNG>
          </div>
        )}
      </div>
    </div>
  );
};