Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Today/Start of Term Calendar Button #396

Open
FlyteWizard opened this issue Jun 14, 2022 · 4 comments
Open

feat: Today/Start of Term Calendar Button #396

FlyteWizard opened this issue Jun 14, 2022 · 4 comments
Labels
enhancement New feature or request good first issue Good for newcomers

Comments

@FlyteWizard
Copy link
Member

Describe the Bug/Issue

The timetable has a Today button that brings users to today's date. However, there is no way to go back to the Start of Term. Users must refresh the page, or select another term and then re-select the desired term.

To Reproduce

Steps to reproduce the behaviour/bug/issue:

  1. Go to the timetable.
  2. Click the Today button.
  3. Refresh page to return to Start of Term.

Expected Behaviour

Be able to go back to the "Start of Term" after selecting Today or navigating away from the "Start of Term".

Screenshots

courseup-today-button.mov

Desktop/Mobile

  • OS: macOS
  • Browser: Safari
  • Version: Latest

Additional Context

Not sure what the expected behaviour should be because it is a weird behaviour.

Discussion Points

  • Remove or keep the Today button.
  • Have Today button only visible on the Start of Term.
  • Have the Start of Term button.
  • Remove the buttons entirely.

Would love to hear from you folks on your ideas and ideal behaviour.

@FlyteWizard FlyteWizard added the bug Something isn't working label Jun 14, 2022
@FlyteWizard
Copy link
Member Author

FlyteWizard commented Jun 14, 2022

I started implementing a fix in this branch: https://github.com/FlyteWizard/courseup/tree/flyte/feat/start-of-term-button

but didn't continue as I'm not sure what the behaviour should be exactly.

start-of-term-button-implementation.mov

@keithradford keithradford added enhancement New feature or request good first issue Good for newcomers and removed bug Something isn't working labels Sep 21, 2022
@keithradford keithradford changed the title Bug: Today Calendar Button feat: Today/Start of Term Calendar Button Sep 21, 2022
@keithradford
Copy link
Collaborator

updating this as it's a good enhancement

@FlyteWizard
Copy link
Member Author

FlyteWizard commented Sep 22, 2022

If anyone wants to take this feel free 👍 I deleted the repository because I didn't think I'd work on this issue again any time soon, but below is the code that I started. It is a rough implementation, but gets the job done 😄

@FlyteWizard
Copy link
Member Author

./src/pages/scheduler/components/SchedulerCalendar.tsx

import { useEffect, useMemo, useState } from 'react';

import 'react-big-calendar/lib/sass/styles.scss';

import { format, getDay, parse, set, startOfWeek } from 'date-fns';
import { enUS } from 'date-fns/locale';
import { Calendar, dateFnsLocalizer } from 'react-big-calendar';

import { useDarkMode } from 'lib/hooks/useDarkMode';
import { useSmallScreen } from 'lib/hooks/useSmallScreen';

import { CalendarEvent } from 'pages/scheduler/components/Event';
import { CalendarToolBar } from 'pages/scheduler/components/Toolbar';
import { useInitialDateTime } from 'pages/scheduler/hooks/useInitialDatetime';
import { useStartOfTermDateTime } from 'pages/scheduler/hooks/useStartOfTermDateTime';
import { coursesToVCalendar } from 'pages/scheduler/shared/exporter';
import { courseCalEventsToCustomEvents } from 'pages/scheduler/shared/transformers';
import { CustomEvent } from 'pages/scheduler/shared/types';
import { eventPropGetter } from 'pages/scheduler/styles/eventPropGetter';
import { slotPropGetter } from 'pages/scheduler/styles/slotPropGetter';

import { CourseCalendarEvent } from '../shared/types';

const localizer = dateFnsLocalizer({
  format,
  parse,
  startOfWeek,
  getDay,
  locales: { 'en-US': enUS },
});

export interface SchedulerCalendarProps {
  /**
   * CalendarEvents
   * Parses events that can go into the calendar from this
   */
  courseCalendarEvents?: CourseCalendarEvent[];
  term: string;
}

export const SchedulerCalendar = ({ term, courseCalendarEvents = [] }: SchedulerCalendarProps): JSX.Element => {
  // for darkmode
  const mode = useDarkMode();
  const today = useMemo(() => new Date(), []);
  const smallScreen = useSmallScreen();
  // initialize selected date
  const [selectedDate, setSelectedDate] = useState(today);
  // state for today button
  const [isToday, setIsToday] = useState(false);
  // initialize initial view
  const [view, setView] = useState<'day' | 'work_week'>('work_week');
  // determine what date to position the calendar on.
  const initialSelectedDate = useInitialDateTime(term);
  // determine the start of the term
  const startOfTermDate = useStartOfTermDateTime(term);

  // determines the max hour to display on the calendar. defaults to 8 pm
  const [maxTime, setMaxTime] = useState<{ hours: number; minutes: number }>({ hours: 20, minutes: 0 });

  // create events compatible with the calendar from courses

  const vCalendar = useMemo(() => {
    return courseCalendarEvents.length !== 0 ? coursesToVCalendar(courseCalendarEvents) : undefined;
  }, [courseCalendarEvents]);

  const events = useMemo(() => {
    const { events, maxHours: maxHour, maxMinutes } = courseCalEventsToCustomEvents(courseCalendarEvents);

    if (maxHour >= maxTime.hours && maxMinutes >= maxTime.minutes) {
      setMaxTime({ hours: maxHour, minutes: maxMinutes });
    }

    return events;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [courseCalendarEvents]);

  useEffect(() => {
    setSelectedDate(initialSelectedDate);
    setIsToday(startOfTermDate.toLocaleDateString() !== initialSelectedDate.toLocaleDateString());
    setView(smallScreen ? 'day' : 'work_week');
  }, [initialSelectedDate, smallScreen, startOfTermDate, courseCalendarEvents.length]);

  return (
    <Calendar<CustomEvent>
      localizer={localizer}
      events={events}
      min={set(today, { hours: 8, minutes: 0 })}
      max={set(today, { hours: maxTime.hours, minutes: maxTime.minutes })}
      views={['work_week', 'day']}
      onView={(view) => view}
      view={view}
      onNavigate={(date) => setSelectedDate(date)}
      date={selectedDate}
      eventPropGetter={eventPropGetter}
      slotPropGetter={slotPropGetter(mode)}
      components={{
        toolbar: CalendarToolBar(setSelectedDate, setIsToday, isToday, startOfTermDate, term, smallScreen, vCalendar),
        event: CalendarEvent,
      }}
      dayLayoutAlgorithm="no-overlap"
      formats={{
        dayFormat: (date: Date, culture: any, localizer: any) => localizer.format(date, 'EEEE', culture),
        dayHeaderFormat: (date: Date, culture: any, localizer: any) => localizer.format(date, 'EE MMM do', culture),
      }}
    />
  );
};

./src/pages/scheduler/components/Toolbar.tsx

import { ChevronLeftIcon, ChevronRightIcon, DownloadIcon } from '@chakra-ui/icons';
import { Button, Flex, HStack, IconButton, Text } from '@chakra-ui/react';
import { ToolbarProps } from 'react-big-calendar';
import { FaRegCalendar, FaRegCalendarAlt } from 'react-icons/fa';

import { Term } from 'lib/fetchers';
import { logEvent } from 'lib/utils/logEvent';

import { ShareButton } from './share/ShareButton';

export const CalendarToolBar =
  (
    onSelectedDateChange: (date: Date) => void,
    onTodayChange: (today: boolean) => void,
    isToday: boolean,
    startOfTerm: Date,
    term: string,
    smallScreen: boolean,
    vCalendar?: string
  ) =>
  ({ label, date }: ToolbarProps) => {
    const handleClick = (offset?: number) => () => {
      if (offset) {
        const d = new Date(date);
        d.setDate(d.getDate() + offset);
        onSelectedDateChange(d);
        if (d.toLocaleDateString() !== startOfTerm.toLocaleDateString()) {
          onTodayChange(true);
        } else {
          onTodayChange(false);
        }
      } else {
        if (isToday) {
          onSelectedDateChange(startOfTerm);
          onTodayChange(false);
        } else {
          onSelectedDateChange(new Date());
          onTodayChange(true);
        }
      }
    };

    const handleDownload = () => {
      if (vCalendar) {
        logEvent('calendar_download', { term });

        const element = document.createElement('a');
        const file = new Blob([vCalendar], { type: 'text/plain' });
        element.href = URL.createObjectURL(file);
        element.download = `${term}_calendar.ics`;
        element.click();
        document.body.removeChild(element);
      }
    };

    return (
      <Flex pb="0.5em" justifyContent="space-between" alignItems="center">
        <Text fontSize={{ base: 'xs', sm: 'xl' }}>{label}</Text>
        <HStack pb="0.2em">
          <ShareButton term={term as Term} disabled={!vCalendar} />
          {smallScreen ? (
            <IconButton
              icon={<DownloadIcon />}
              aria-label="Download timetable"
              size="sm"
              colorScheme="blue"
              onClick={handleDownload}
              disabled={!vCalendar}
            />
          ) : (
            <Button size="sm" colorScheme="blue" onClick={handleDownload} disabled={!vCalendar}>
              Download
            </Button>
          )}
          {smallScreen ? (
            <IconButton
              aria-label={isToday ? 'Start of Term' : 'Today'}
              colorScheme="gray"
              icon={isToday ? <FaRegCalendarAlt /> : <FaRegCalendar />}
              size="sm"
              onClick={handleClick()}
            />
          ) : (
            <Button size="sm" colorScheme="gray" onClick={handleClick()}>
              {isToday ? 'Start of Term' : 'Today'}
            </Button>
          )}

          <IconButton
            aria-label="Previous Week"
            colorScheme="gray"
            icon={<ChevronLeftIcon />}
            size="sm"
            onClick={handleClick(smallScreen ? -1 : -7)}
          />
          <IconButton
            aria-label="Next Week"
            colorScheme="gray"
            icon={<ChevronRightIcon />}
            size="sm"
            onClick={handleClick(smallScreen ? 1 : 7)}
          />
        </HStack>
      </Flex>
    );
  };

./src/pages/scheduler/hooks/useStartOfTermDateTime.tsx

import { useMemo } from 'react';

export const useStartOfTermDateTime = (term: string) => {
  // determine the start of the term (i.e., the first day of the month)
  return useMemo<Date>(() => {
    // eg. 202105 => 2021, 05
    const month = parseInt(term.substring(4, 6));
    const year = parseInt(term.substring(0, 4));
    return new Date(year, month - 1, 1);
  }, [term]);
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request good first issue Good for newcomers
Projects
None yet
Development

No branches or pull requests

3 participants