Datepicker.jsx

import React from 'react'
import { useState } from 'react'
import {createUseStyles} from 'react-jss'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
  faArrowLeft,
  faArrowRight,
  faCaretDown,
  faCaretUp,
} from '@fortawesome/free-solid-svg-icons'
import formatDateUS from '../dateFormat'

const dateNow = new Date()
const yearNow = dateNow.getFullYear()

/**
 * CSS definition of the date picker /
 * Using package react-jss
 *
 * @namespace
 * @author  Pierre-Yves Léglise <pleglise@pm.me>
 * 
 */
const useStyles = createUseStyles({
  "cal-button": {
    "font-family": 'sans-serif',
    "align-self": "center",
    "padding": "0.25rem",
    "font-size": "0.75rem",
    "border-radius": "0.25rem",
    "background-color": "#3a33a4",
    "color": "white"
  },
  "cal-button:hover": {
    "background-color": "white",
    "color": "black",
    "box-shadow": "0 2px 10px 5px #bcbcbc"
  },
  "darkBG": {
    "font-family": 'sans-serif',
    "font-size": "1em",
    "background-color": "rgba(0, 0, 0, 0.2)",
    "width": "100%",
    "height": "100%",
    "overflow": "hidden",
    "z-index": "0",
    "position": "absolute",
    "top": "0",
    "left": "0"
  },
  "cal-cell": {
    "font-family": 'sans-serif',
    "font-size": "1em",
    "cursor": "pointer",
    "display": "flex",
    "align-items": "center",
    "justify-content": "center",
    "width": "100%",
    "border-radius": "0.25rem"
  },
  "cal-cell:hover": {
    "background-color": "#082450",
    "color": "white"
  },
  "year-grid": {
    "font-family": 'sans-serif',
    "display": "grid",
    "grid-template-columns": "repeat(5, minmax(0, 1fr))",
    "width": "100%",
    "padding-right": "0.25rem",
    "cursor": "default",
    "font-size": "1rem",
    "line-height": "1.5rem",
    "text-align": "center",
    "height": "9.6em",
    "overflow-y": "auto"
  },
  "days-header": {
    "font-family": 'sans-serif',
    "font-size": "1em",
    "display": "grid",
    "grid-template-columns": "repeat(7, minmax(0, 1fr))",
    "gap": "0.25rem",
    "border-bottom-width": "2px",
    "text-align": "center",
    "cursor": "default"
  },
  "days-grid": {
    "font-family": 'sans-serif',
    "font-size": "1em",
    "display": "grid",
    "margin-top": "0.25rem",
    "grid-template-columns": "repeat(7, minmax(0, 1fr))",
    "gap": "0.25rem",
    "text-align": "center"
  },
  "day-selected": {
    "font-weight": "700",
    "background-color": "rgba(0, 0, 0, 0.2)"
  },
  "year-selected": {
    "font-weight": "700",
    "background-color": "rgba(0, 0, 0, 0.2)"
  },
  "month-grid": {
    "font-family": 'sans-serif',
    "font-size": "1em",
    "display": "grid",
    "grid-template-columns": "repeat(3, minmax(0, 1fr))",
    "width": "100%",
    "padding-right": "0.25rem",
    "cursor": "default",
    "line-height": "1.5rem",
    "text-align": "center",
    "height": "9em",
    "overflow-y": "auto"
  },
  "month-selected": {
    "font-size": ".95em",
    "font-weight": "700",
    "background-color": "rgba(0, 0, 0, 0.2)"
  },
  "italic": {
    "font-style": "italic"
  },
  "nav-container": {
    "font-family": 'sans-serif',
  "font-size": "1em",
    'margin-bottom':'0.5em',
    "display": "flex",
    "justify-content": "space-between"
  },
  "nav-buttons": {
    "font-family": 'sans-serif',
    "font-size": "1em",
    margin:0,
    "display": "flex",
    "gap": "0.5rem",
    "align-self": "center",
    "font-weight": "700"
  },
  "nav-button": {
    "font-family": 'sans-serif',
    "font-size": "1em",
    margin:'0.5em',
    "padding-left": "0.25rem",
    "padding-right": "0.25rem",
    "cursor": "pointer"
  },
  "nav-button:hover": {
    "border-radius": "0.25rem",
    "color": "#ffffff",
    "background-color": "#082450"
  },
  "main-container": {
    "font-family": 'sans-serif',
    "font-size": "1em",
    "position": "fixed",
    "border-radius": "0.5rem",
    "width": "auto",
    "height": "auto",
    "background-color": "white",
    "box-shadow": "0 5px 20px 0px #00011c"
  },
  "date-picker-container": {
    "font-family": 'sans-serif',
    "font-size": "1em",
    "padding-top": "0.5rem",
    "padding-bottom": "0.5rem",
    "margin-left": "0.75rem",
    "margin-right": "0.75rem",
    "text-align": "left",
    "width": "16.3em",
    "height": "auto"
  },
  "margin-left": {
    "margin-left": "0.25rem"
  }
}
)

/**
 * Component that displays a date picker
 *
 * @namespace
 * @component
 * @author  Pierre-Yves Léglise <pleglise@pm.me>
 * @example
 * import { DatePicker } from 'date-picker-nextjs'
import { useState } from 'react'

const Example = () => {
  const [modalDateIsOpen, setModalDateIsOpen] = useState(false)
  const [clickedInput, setClickedInput] = useState(null)

  const handleDatePicker = (e) => {
    setClickedInput(e.target.id)
    setModalDateIsOpen(true)
  }

  const submit = (e) => {
    e.preventDefault()
    // your logic
  }

  return (
    <form
      className='test'
      onSubmit={submit}
    >
      <label htmlFor='birthdate'>Birthdate</label>
      <input
        className='input-field outline-none'
        type='text'
        id='dateOfBirth'
        placeholder='Date of birth'
        onClick={handleDatePicker}
      />

      <input
        type='submit'
        value='Submit'
      />
    </form>
    {modalDateIsOpen && (
        <DatePicker
          setModalDateIsOpen={setModalDateIsOpen}
          clickedInput={clickedInput}
        />
      )}
  )
}

export default Example
 * @prop {Object}     setModalIsOpen                 State function used to close the modal.
 * @prop {String}     clickedInput                   The id of the input filed to attach the date picker modal to
 * @prop {Number}     endYear                    (optionnal) The last year to display. Default : current year
 * @prop {Number}     yearCount                    (optionnal) The number of years to display. Default : 100
 * 
 * @returns {JSX.Element}   A JSX.Element that contains the modal.
 */
const DatePicker=({
  setModalDateIsOpen,
  clickedInput,
  endYear = yearNow,
  yearCount = 100,
}) =>{
  const styles=useStyles()
  const [selectedDate, setSelectedDate] = useState(new Date())
  const [yearSelectIsOpen, setYearSelectIsOpen] = useState(false)
  const [monthSelectIsOpen, setMonthSelectIsOpen] = useState(false)

  /**
   * Change the selected month by a specified amount.
   *
   * @param {number} amount - The amount to change the month by.
   */
  const changeMonth = (amount) => {
    const newDate = new Date(selectedDate)
    newDate.setMonth(selectedDate.getMonth() + amount)
    setSelectedDate(newDate)
  }
/**
   * Change the selected year to a specific year.
   *
   * @param {number} year - The year to set as the selected year.
   */
  const changeYear = (year) => {
    const newDate = new Date(selectedDate)
    newDate.setFullYear(year)
    setSelectedDate(newDate)
    toggleYearScreen()
  }
 /**
   * Select a specific month.
   *
   * @param {number} month - The month to select.
   */
  const selectMonth = (month) => {
    const newDate = new Date(selectedDate)
    newDate.setMonth(month)
    setSelectedDate(newDate)
    toggleMonthScreen()
  }
/**
   * Select a specific date and set it as the input value.
   *
   * @param {Date} date - The selected date.
   */
  const selectDate = (date) => {
    document.getElementById(clickedInput).value = formatDateUS(date)
    setModalDateIsOpen(false)
  }
  /**
   * Toggle the year selection screen.
   */
  const toggleYearScreen = () => {
    setYearSelectIsOpen(!yearSelectIsOpen)
    setMonthSelectIsOpen(false)
  }
  /**
   * Toggle the month selection screen.
   */
  const toggleMonthScreen = () => {
    setMonthSelectIsOpen(!monthSelectIsOpen)
    setYearSelectIsOpen(false)
  }
/**
   * Get the day of the week for the first day of the selected month.
   *
   * @returns {number} The day of the week for the first day.
   */
  const getFirstDayOfWeek = () => {
    const firstDay = new Date(
      selectedDate.getFullYear(),
      selectedDate.getMonth(),
      1
    )
    if (firstDay.getDay() === 0) return 7
    else return firstDay.getDay()
  }

  /**
   * Generate the years to be displayed.
   *
   * @returns {JSX.Element} JSX elements representing the years.
   */
  const generateYears = () => {
    const today = new Date()
    const yearToday = today.getFullYear()
    const years = []
    for (let year = endYear - yearCount; year <= endYear; year++) {
      years.push(
        <div
          key={`year-${year}`}
          id={`year-${year}`}
          onClick={() => changeYear(year)}
          className={styles['cal-cell'] + (yearToday === year ? ' '+styles['year-selected'] : '')}
        >
          {year}
        </div>
      )
    }
    return <div className={styles["year-grid"]}>{years.reverse()}</div>
  }

   /**
   * Generate the months to be displayed.
   *
   * @returns {JSX.Element} JSX elements representing the months.
   */
  const generateMonths = () => {
    const today = new Date()
    const monthToday = today.getMonth()
    const months = []
    const yearToday = today.getFullYear()
    const selectedYear = selectedDate.getFullYear()
    for (let month = 1; month <= 12; month++) {
      const date = new Date(Date.UTC(2000, month, 1))
      const monthString = date.toLocaleString('default', { month: 'long' })
      months.push(
        <div
          key={`month-${month}`}
          id={`month-${month}`}
          onClick={() => selectMonth(month - 1)}
          className={
            styles['cal-cell'] +
            (monthToday === month - 1 && yearToday === selectedYear
              ? ' '+styles['month-selected']
              : '')
          }
        >
          {monthString}
        </div>
      )
    }
    return (
      <div
        className={styles["month-grid"]}
      >
        {months}
      </div>
    )
  }

  /**
   * Generate the days to be displayed.
   *
   * @returns {JSX.Element} JSX elements representing the days.
   */
  const generateDays = () => {
    const days = []
    const firstDayOfWeek = getFirstDayOfWeek()
    const lastDay = new Date(
      selectedDate.getFullYear(),
      selectedDate.getMonth() + 1,
      0
    )
    const today = new Date()
    const dayToday = today.getDate()
    const yearToday = today.getFullYear()
    const selectedYear = selectedDate.getFullYear()
    const monthToday = today.getMonth()
    const selectedMonth = selectedDate.getMonth()

    for (let i = 1; i < firstDayOfWeek; i++) {
      days.push(<div key={`empty-${i}`}></div>)
    }

    for (let date = 1; date <= lastDay.getDate(); date++) {
      days.push(
        <div
          key={date}
          className={
            styles['cal-cell'] +
            (dayToday === date &&
            monthToday === selectedMonth &&
            yearToday === selectedYear
              ? ' '+styles['day-selected']
              : '')
          }
          onClick={() =>
            selectDate(
              new Date(
                selectedDate.getFullYear(),
                selectedDate.getMonth(),
                date
              )
            )
          }
        >
          {date}
        </div>
      )
    }

    return (
      <>
        <div className={styles["days-header"]}>
          <div>Mon</div>
          <div>Tue</div>
          <div>Wed</div>
          <div>Thu</div>
          <div>Fri</div>
          <div className="italic">Sat</div>
          <div className="italic">Sun</div>
        </div>
        <div className={styles["days-grid"]}>{days}</div>
      </>
    )
  }
  /**
   * Render the navigator for the date picker.
   *
   * @returns {JSX.Element} JSX elements for the date picker navigator.
   */
  const datePickerNavigator = () => {
    return (
      <div className={styles["nav-container"]}>
        <FontAwesomeIcon
          className={styles["cal-button"]}
          icon={faArrowLeft}
          onClick={() => changeMonth(-1)}
        />
        <div className={styles["nav-buttons"]}>
          <div>
            <p className={styles["nav-button"]} onClick={toggleMonthScreen}>
              {selectedDate.toLocaleDateString('default', {
                month: 'long',
              })}
              <span className={styles["margin-left"]}>
                <FontAwesomeIcon
                  icon={monthSelectIsOpen ? faCaretDown : faCaretUp}
                />
              </span>
            </p>
          </div>
          <div>
            <p className={styles["nav-button"]} onClick={toggleYearScreen}>
              {selectedDate.toLocaleDateString('default', {
                year: 'numeric',
              })}
              <span className={styles["margin-left"]}>
                <FontAwesomeIcon
                  icon={yearSelectIsOpen ? faCaretDown : faCaretUp}
                />
              </span>
            </p>
          </div>
        </div>
        <FontAwesomeIcon
          className={styles["cal-button"]}
          icon={faArrowRight}
          onClick={() => changeMonth(1)}
        />
      </div>
    )
  }

  if (clickedInput) {
    const inputElement = document.getElementById(clickedInput)
    const inputRect = inputElement.getBoundingClientRect()
    const topOffset = inputRect.top + window.scrollY + 32
    const leftOffset = inputRect.left + window.scrollX

    return (
      <div
        className={styles["darkBG"]}
        onClick={(e) => {
          e.target.className === styles['darkBG'] && setModalDateIsOpen(false)
        }}
      >
        <div
          className={styles["main-container"]}
          style={{ top: topOffset, left: leftOffset }}
        >
          <div className={styles["date-picker-container"]}>
            {datePickerNavigator()}
            <div>
              {yearSelectIsOpen
                ? generateYears()
                : monthSelectIsOpen
                ? generateMonths()
                : generateDays()}
            </div>
          </div>
        </div>
      </div>
    )
  }
}
export default DatePicker