components/Calendar.js

/* eslint-disable react/no-array-index-key */

import React, { Component } from 'react';
import {
  daysInMonth,
  weekDayInMonth,
  getDateForCalendar,
  getMonth,
  getDateForFilter,
  getCountryNewDate,
} from '../utils/common';
import SVGComponent from './SVG/SVGComponent';
import { SVG_ICONS } from '../constants';

/**
 * @module Calendar
 */
/**
 * @typedef {object} props
 * @property {string} className
 * @property {boolean} sub
 * @property {string} field
 * @property {object} filter
 * @property {object} defaultFilter
 * @property {boolean} actions
 * @property {Time} inactivePreviousDates Use when date need to be inactive
 * before some date.
 * @property {Function} changeFilter
 */
export default class Calendar extends Component {
  constructor(props) {
    super(props);
    /**
     * @member {object}
     * @property {Time} selectedDate
     * @property {boolean} isOpen
     * @property {Time} date
     */
    const defaultDate = props.filter[props.field];
    const selectedDate = defaultDate
      ? getCountryNewDate(this.props.selectedTimezone, defaultDate)
      : getCountryNewDate(this.props.selectedTimezone);

    this.state = {
      month: selectedDate,
      selectedDate,
      isOpen: false,
      date: defaultDate ? getCountryNewDate(this.props.selectedTimezone, defaultDate) : null,
    };

    /**
     * @member {Array}
     */
    this.inactiveDatesBefore = [];
    /**
     * @member {Array}
     */
    this.activeDates = [];
    /**
     * @member {Array}
     */
    this.inactiveDatesAfter = [];

    // COMPONENT WILL MOUNT
    this.setDataForOneMonth(this.state.selectedDate);
  }

  /**
   * Update state on clear filter
   *
   * @param {object} prevProps
   * @returns {void}
   */
  componentDidUpdate(prevProps) {
    if (this.props.filter[this.props.field] !== prevProps.filter[prevProps.field]) {
      const defaultDate = this.props.filter[this.props.field];
      const selectedDate = defaultDate
        ? getCountryNewDate(this.props.selectedTimezone, defaultDate)
        : getCountryNewDate(this.props.selectedTimezone);

      this.setDataForOneMonth(selectedDate);
      // eslint-disable-next-line react/no-did-update-set-state
      this.setState({
        selectedDate,
        isOpen: false,
        date: defaultDate ? getCountryNewDate(this.props.selectedTimezone, defaultDate) : null,
        month: selectedDate,
      });
    }
  }

  /**
   * Toggle calendar
   *
   * @function
   * @returns {void}
   */
  toggleCalendarPopup = () => {
    if (!this.state.isOpen) {
      this.setState({
        isOpen: true,
      });
    } else {
      this.cancel();
    }
  };

  /**
   * Set previous month
   *
   * @function
   * @returns {void}
   */
  setPreviousMonth = () => {
    const month = this.state.month;
    if (this.props.inactivePreviousDates && this.props.inactivePreviousDates.getMonth() === month.getMonth()) {
      return;
    }
    month.setMonth(month.getMonth() - 1, 1);
    this.setDataForOneMonth(month);
    this.setState({
      month,
    });
  };

  /**
   * Set next month
   *
   * @function
   * @returns {void}
   */
  setNextMonth = () => {
    const month = this.state.month;
    month.setMonth(month.getMonth() + 1, 1);
    this.setDataForOneMonth(month);
    this.setState({
      month,
    });
  };

  /**
   * Change day
   *
   * @function
   * @param {number} day
   * @returns {void}
   */
  setDay = (day) => {
    const date = this.state.selectedDate;
    date.setFullYear(this.state.month.getFullYear(), this.state.month.getMonth(), day);
    this.setState(
      {
        selectedDate: date,
        month: new Date(date.getTime()),
      },
      () => {
        if (!this.props.actions) {
          this.applay('setDay');
        }
      }
    );
  };

  /**
   * Set default filter
   *
   * @function
   * @returns {void}
   */
  reset = () => {
    let date;
    if (this.props.defaultFilter && this.props.defaultFilter[this.props.field]) {
      date = getCountryNewDate(this.props.selectedTimezone, this.props.defaultFilter[this.props.field]);
      this.props.changeFilter(getDateForFilter(date), this.props.field);
    }
    const selectedDate = date
      ? getCountryNewDate(this.props.selectedTimezone, date.getTime())
      : getCountryNewDate(this.props.selectedTimezone);
    this.setState({
      isOpen: false,
      date,
      selectedDate,
      month: getCountryNewDate(this.props.selectedTimezone, selectedDate.getTime()),
    });
  };

  /**
   * Discard changes
   *
   * @function
   * @returns {void}
   */
  cancel = () => {
    const selectedDate = this.state.date ? this.state.date : new Date();

    this.setState({
      isOpen: false,
      selectedDate,
    });
  };

  /**
   * Change date
   *
   * @function
   * @param {string} type
   * @returns {void}
   */
  applay = (type) => {
    this.props.changeFilter(getDateForFilter(this.state.selectedDate), this.props.field);
    this.setState({
      isOpen: false,
      date: this.state.selectedDate
        ? type && type === 'setDay'
          ? new Date(this.state.selectedDate.getTime())
          : getCountryNewDate(this.props.selectedTimezone, this.state.selectedDate.getTime())
        : null,
    });
  };

  /**
   * Calculate month view
   *
   * @function
   * @param {Time} date
   */
  setDataForOneMonth = (date) => {
    const copiedDate = new Date(date.getTime());

    let numOfDays = daysInMonth(copiedDate.getMonth() + 1, copiedDate.getFullYear());
    const startWith = weekDayInMonth(copiedDate.getMonth(), copiedDate.getFullYear());
    copiedDate.setFullYear(copiedDate.getFullYear(), copiedDate.getMonth() - 1, 1);
    let numOfDaysPreviousMonth = daysInMonth(copiedDate.getMonth() + 1, copiedDate.getFullYear());
    numOfDaysPreviousMonth -= startWith;
    this.inactiveDatesBefore = Array(startWith)
      .fill()
      .map(() => {
        numOfDaysPreviousMonth += 1;
        return numOfDaysPreviousMonth;
      });

    let counter = 0;
    if (this.props.inactivePreviousDates && this.props.inactivePreviousDates.getMonth() === date.getMonth()) {
      const numOfInvalidDates = this.props.inactivePreviousDates.getDate() - 1;
      Array(numOfInvalidDates)
        .fill()
        .map(() => {
          counter += 1;
          this.inactiveDatesBefore.push(counter);
        });
      numOfDays -= numOfInvalidDates;
    }

    this.activeDates = Array(numOfDays)
      .fill()
      .map(() => {
        counter += 1;
        return counter;
      });
    let i = 0;
    this.inactiveDatesAfter = Array(42 - counter - startWith)
      .fill()
      .map(() => {
        i += 1;
        return i;
      });
  };

  /**
   * Parse date objects
   *
   * @function
   * @returns {object}
   */
  getCalendarData = () => {
    const dateTime = new Date();
    const fullDate = getDateForCalendar(dateTime);
    let date = getDateForCalendar(this.state.date);
    date = date.slice(0, -4) + date.slice(date.length - 2, date.length);
    const month = getMonth(this.state.month);
    const currentDay = this.state.month.getMonth() === dateTime.getMonth() ? this.state.selectedDate.getDate() : -1;
    return {
      currentDay,
      date,
      month,
      fullDate,
    };
  };

  /**
   * Render
   *
   * @returns {view}
   */
  render() {
    const data = this.getCalendarData();
    return (
      <>
        <div className={this.props.className}>
          <div className="calendar__wrapper d-flex align-items-center" onClick={this.toggleCalendarPopup}>
            <div className="calendar__input-date">
              <div className="mr-2">{data.date}</div>
            </div>
            <i className={`icon-calendar ${this.props.sub && 'green'}`} onClick={this.toggleCalendarPopup} />
          </div>
          <div className={`calendar__popup ${this.state.isOpen && 'open'} ${this.props.sub && 'sub bg-grey'}`}>
            <div className="p-15">
              <div className="calendar__popup-heading">
                <h3>{data.fullDate}</h3>
                <SVGComponent
                  className={`icon-xxs calendar__popup-close${this.props.sub ? '-sub' : ''}`}
                  src={`${SVG_ICONS.utility}#close`}
                  onClick={this.toggleCalendarPopup}
                />
              </div>
            </div>
            {/* calendar__popup-date */}
            <div className="calendar__popup-row active">
              <div>{data.month}</div>
              <div className="ml-auto">
                <SVGComponent
                  className="icon-xxs prev-icon"
                  src={`${SVG_ICONS.pagination}#arrow`}
                  onClick={this.setPreviousMonth}
                />
                <SVGComponent
                  className="icon-xxs next-icon ml-2"
                  src={`${SVG_ICONS.pagination}#arrow`}
                  onClick={this.setNextMonth}
                />
              </div>
            </div>
            <div className="calendar__popup-table">
              <div className="calendar__popup-table-cell">S</div>
              <div className="calendar__popup-table-cell">M</div>
              <div className="calendar__popup-table-cell">T</div>
              <div className="calendar__popup-table-cell">W</div>
              <div className="calendar__popup-table-cell">T</div>
              <div className="calendar__popup-table-cell">F</div>
              <div className="calendar__popup-table-cell">S</div>
              {this.inactiveDatesBefore.map((day, idx) => (
                <div className="calendar__popup-table-cell date inactive" key={`inactiveBefore-${day}-${idx}`}>
                  {day}
                </div>
              ))}
              {this.activeDates.map((day, idx) => (
                <div
                  key={`active-${day}-${idx}`}
                  className={`calendar__popup-table-cell date ${data.currentDay === day ? 'selected' : ''}`}
                  onClick={this.setDay.bind(this, day)}
                >
                  {day}
                </div>
              ))}
              {this.inactiveDatesAfter.map((day, idx) => (
                <div className="calendar__popup-table-cell date inactive" key={`inactiveAfter-${day}-${idx}`}>
                  {day}
                </div>
              ))}
            </div>
            {this.props.actions && (
              <div className="p-15">
                <div className="calendar__popup-button">
                  <button type="button" className="btn btn-secondary mr-auto" onClick={this.reset}>
                    Reset
                  </button>
                  <button type="button" className="btn btn-secondary" onClick={this.cancel}>
                    Cancel
                  </button>
                  <button type="button" onClick={this.applay} className="btn btn-primary ml-10">
                    Apply
                  </button>
                </div>
              </div>
            )}
          </div>
        </div>
        {this.state.isOpen && <div className="overlay-mask bg-grey open" />}
      </>
    );
  }
}