ahbeng/NUSMods

View on GitHub
website/src/views/timetable/ExamWeek.tsx

Summary

Maintainability
A
2 hrs
Test Coverage
import * as React from 'react';
import { Link } from 'react-router-dom';
import { range } from 'lodash';
import { isSameDay, addDays } from 'date-fns';

import { ModuleWithColor, ModuleWithExamTime, TIME_SEGMENTS } from 'types/views';
import { formatExamDate } from 'utils/modules';
import { modulePage } from 'views/routes/paths';
import useCurrentTime from 'views/hooks/useCurrentTime';

import styles from 'views/timetable/ExamCalendar.scss';

const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

type Props = {
  // Map of exam dates to modules with the same exam
  modules: Record<string, ModuleWithExamTime[]>;
  weekNumber: number;
  firstDayOfExams: Date;
  days: number;
};

function getExamDate(date: Date): string {
  return formatExamDate(date.toISOString()).split(' ')[0];
}

const ExamModule: React.FC<{ module: ModuleWithColor }> = ({ module }) => (
  <Link
    to={modulePage(module.moduleCode, module.title)}
    className={`hoverable color-${module.colorIndex}`}
  >
    <div className={styles.moduleCode}>{module.moduleCode}</div>
    <div className={styles.moduleTitle}>{module.title}</div>
  </Link>
);

const ExamWeekComponent: React.FC<Props> = (props) => {
  const { modules, weekNumber, firstDayOfExams, days } = props;
  const currentTime = useCurrentTime();

  // Array of dates to display
  const dayDates = range(days).map((offset) => addDays(firstDayOfExams, weekNumber * 7 + offset));

  // Start counting months using the last day of the previous week, or null if
  // this is the very first week, so the month always gets displayed on the
  // first cell
  const lastDayOfLastWeekOffset = (weekNumber - 1) * 7 + days - 1;
  let currentMonth =
    weekNumber === 0 ? null : addDays(firstDayOfExams, lastDayOfLastWeekOffset).getMonth();

  // Each week consists of a header row and three module rows, one for each possible time segment
  const headerRow = (
    <tr className={styles.timeRow}>
      {dayDates.map((date) => {
        // Show the month name when the month changes on the calendar
        let examDateString = String(date.getUTCDate());
        if (currentMonth !== date.getMonth()) {
          examDateString = `${MONTHS[date.getUTCMonth()]} ${examDateString}`;
          currentMonth = date.getMonth();
        }
        return (
          <th className={styles.dayDate} key={examDateString}>
            <time dateTime={date.toDateString()}>{examDateString}</time>
            {isSameDay(currentTime, date) && <span className={styles.todayBadge}>Today</span>}
          </th>
        );
      })}
    </tr>
  );

  const moduleRows = TIME_SEGMENTS.map((timeSegment) => (
    <tr key={timeSegment}>
      {dayDates.map((date) => {
        // Get modules with exams on this day and time segment
        const modulesOnThisDay = modules[getExamDate(date)] || [];
        const modulesAtThisTime = modulesOnThisDay.filter(
          (module) => module.timeSegment === timeSegment,
        );

        return (
          <td className={styles.day} key={date.getTime()}>
            {!!modulesAtThisTime.length && (
              <>
                <h4>{modulesAtThisTime[0].time}</h4>
                {modulesAtThisTime.map(({ module }) => (
                  <ExamModule key={module.moduleCode} module={module} />
                ))}
              </>
            )}
          </td>
        );
      })}
    </tr>
  ));

  return (
    <>
      {headerRow}
      {moduleRows}
    </>
  );
};

export default ExamWeekComponent;