Skip to content

Instantly share code, notes, and snippets.

@stevensacks
Last active June 26, 2023 08:59
Show Gist options
  • Save stevensacks/79c60d0f8b1f8bc06b475438f59d687e to your computer and use it in GitHub Desktop.
Save stevensacks/79c60d0f8b1f8bc06b475438f59d687e to your computer and use it in GitHub Desktop.

Revisions

  1. stevensacks revised this gist Jan 6, 2019. 3 changed files with 15 additions and 6 deletions.
    13 changes: 10 additions & 3 deletions DatePicker.js
    Original file line number Diff line number Diff line change
    @@ -2,15 +2,17 @@ import {addDays, format, isAfter, isBefore, startOfDay} from 'date-fns';
    import React, {Component, Fragment} from 'react';
    import classes from 'classnames';
    import DatePickerDialog from './DatePickerDialog';
    import {Dialog} from 'core';
    import Dialog from 'components/Dialog';
    import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
    import {getDisabled} from '../../../utils/component';
    import PropTypes from 'prop-types';
    import './index.css';

    export default class DatePicker extends Component {
    static propTypes = {
    className: PropTypes.string,
    dateFormat: PropTypes.string,
    disabled: PropTypes.bool,
    endDate: PropTypes.instanceOf(Date),
    isRange: PropTypes.bool,
    maxDate: PropTypes.instanceOf(Date),
    @@ -78,17 +80,22 @@ export default class DatePicker extends Component {
    );

    render() {
    const {className, dateFormat, isRange} = this.props;
    const {className, dateFormat, disabled, isRange} = this.props;
    const {startDate, endDate} = this.state;
    const formattedStartDate = format(startDate, dateFormat);
    const formattedEndDate = isRange ? format(endDate, dateFormat) : '';
    const click = !disabled ? this.onClick : undefined;
    return (
    <div
    className={classes('date-picker', {
    [className]: !!className,
    })}
    >
    <a className="button date-picker-button" onClick={this.onClick}>
    <a
    className="button date-picker-button"
    onClick={click}
    {...getDisabled(disabled)}
    >
    <span className="icon">
    <FontAwesomeIcon icon={['fas', 'calendar-alt']} />
    </span>
    4 changes: 2 additions & 2 deletions DatePickerCalendar.js
    Original file line number Diff line number Diff line change
    @@ -56,8 +56,8 @@ export const DatePickerCalendar = ({
    const onClick = !isValidDate(theDate, minDate, maxDate)
    ? null
    : isThisMonth
    ? onClickDate
    : onClickJump;
    ? onClickDate
    : onClickJump;
    return {
    date: theDate,
    isToday: isToday(theDate),
    4 changes: 3 additions & 1 deletion DatePickerNav.js
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,9 @@
    import React, {Fragment} from 'react';
    import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
    import {format} from 'date-fns';
    import PropTypes from 'prop-types';
    import React, {Fragment} from 'react';

    /* eslint-disable jsx-a11y/anchor-is-valid */

    export const DatePickerNav = ({
    visibleDate,
  2. stevensacks revised this gist Aug 19, 2018. 1 changed file with 5 additions and 4 deletions.
    9 changes: 5 additions & 4 deletions DatePicker.js
    Original file line number Diff line number Diff line change
    @@ -32,13 +32,14 @@ export default class DatePicker extends Component {
    !props.minDate || isAfter(props.startDate, props.minDate)
    ? props.startDate
    : props.minDate;
    const endDate =
    !props.maxDate || isBefore(props.endDate, props.maxDate)
    const endDate = props.isRange
    ? !props.maxDate || isBefore(props.endDate, props.maxDate)
    ? props.endDate
    : props.maxDate;
    : props.maxDate
    : undefined;
    this.state = {
    startDate: startOfDay(startDate),
    endDate: props.isRange ? startOfDay(endDate) : undefined,
    endDate: endDate ? startOfDay(endDate) : undefined,
    };
    }

  3. stevensacks revised this gist Aug 19, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,7 @@
    # DatePicker
    The DatePicker component allows you to pick a single date, or date range (from -> to).

    This component is built on top of my [Dialog as a promise component](https://gist.github.com/stevensacks/b2fb14ef89e33aecdb0ca0a7a8b4f11a).
    This component is built on top of my [Dialog as a promise component](https://gist.github.com/stevensacks/b2fb14ef89e33aecdb0ca0a7a8b4f11a) and uses [Bulma](https://bulma.io).

    It uses the SASS from [Bulma Calendar](https://wikiki.github.io/components/calendar/), converted to SCSS.

  4. stevensacks revised this gist Aug 19, 2018. 3 changed files with 11 additions and 5 deletions.
    6 changes: 3 additions & 3 deletions DatePickerDialog.js
    Original file line number Diff line number Diff line change
    @@ -122,9 +122,9 @@ export default class DatePickerDialog extends Component {
    } else if (isAfter(date, endDate)) {
    // extend the end date
    newDates.endDate = date;
    } else if (isAfter(date, startDate)) {
    // truncate start date
    newDates.startDate = date;
    } else if (isBefore(date, endDate)) {
    // truncate end date
    newDates.endDate = date;
    }
    this.setState({...newDates, visibleDate: date});
    this.props.onUpdate(newDates);
    4 changes: 3 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,9 @@ The DatePicker component allows you to pick a single date, or date range (from -

    This component is built on top of my [Dialog as a promise component](https://gist.github.com/stevensacks/b2fb14ef89e33aecdb0ca0a7a8b4f11a).

    It uses the SASS from [Bulma Calendar](https://wikiki.github.io/components/calendar/), but the source Javascript has been rewritten from scratch for use in React.
    It uses the SASS from [Bulma Calendar](https://wikiki.github.io/components/calendar/), converted to SCSS.

    The Javascript has been rewritten from scratch for use in React and uses the [date-fns](https://date-fns.org/docs/) library.

    **Required Props**
    * `onChange` - When you select a date or a date range, this returns the selection (see below).
    6 changes: 5 additions & 1 deletion index.scss
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@
    @import 'css/vars.scss';
    @import 'bulma/sass/utilities/functions';
    @import 'bulma/sass/utilities/derived-variables';

    @@ -30,6 +29,7 @@
    .modal-card {
    display: block;
    width: auto;
    height: 403px;
    }

    .modal-card-body {
    @@ -42,6 +42,10 @@
    display: flex;
    padding: 0.5em;
    }

    .modal-card {
    height: 487px;
    }
    }
    }
    }
  5. stevensacks revised this gist Aug 19, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,7 @@ The DatePicker component allows you to pick a single date, or date range (from -

    This component is built on top of my [Dialog as a promise component](https://gist.github.com/stevensacks/b2fb14ef89e33aecdb0ca0a7a8b4f11a).

    It uses the SASS from [Bulma Calendar](https://wikiki.github.io/components/calendar/), rewritten entirely for use in React.
    It uses the SASS from [Bulma Calendar](https://wikiki.github.io/components/calendar/), but the source Javascript has been rewritten from scratch for use in React.

    **Required Props**
    * `onChange` - When you select a date or a date range, this returns the selection (see below).
  6. stevensacks revised this gist Aug 19, 2018. 2 changed files with 38 additions and 6 deletions.
    10 changes: 4 additions & 6 deletions DatePicker.js
    Original file line number Diff line number Diff line change
    @@ -16,7 +16,7 @@ export default class DatePicker extends Component {
    maxDate: PropTypes.instanceOf(Date),
    minDate: PropTypes.instanceOf(Date),
    name: PropTypes.string,
    onSelect: PropTypes.func.isRequired,
    onChange: PropTypes.func.isRequired,
    startDate: PropTypes.instanceOf(Date),
    };

    @@ -42,8 +42,6 @@ export default class DatePicker extends Component {
    };
    }

    // This is my custom Dialog opener
    // it opens a Bulma modal with a modal-card
    onClick = () =>
    Dialog({
    title: '',
    @@ -64,14 +62,14 @@ export default class DatePicker extends Component {
    endDate: this.state.endDate,
    },
    },
    }).then(this.onSelect);
    }).then(this.onChange);

    onSelect = event =>
    onChange = event =>
    event &&
    this.setState(
    {startDate: event.startDate, endDate: event.endDate},
    () =>
    this.props.onSelect({
    this.props.onChange({
    name: this.props.name,
    startDate: this.state.startDate,
    endDate: this.state.endDate,
    34 changes: 34 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,34 @@
    # DatePicker
    The DatePicker component allows you to pick a single date, or date range (from -> to).

    This component is built on top of my [Dialog as a promise component](https://gist.github.com/stevensacks/b2fb14ef89e33aecdb0ca0a7a8b4f11a).

    It uses the SASS from [Bulma Calendar](https://wikiki.github.io/components/calendar/), rewritten entirely for use in React.

    **Required Props**
    * `onChange` - When you select a date or a date range, this returns the selection (see below).

    **Optional Props**
    * `name` - The data name value which will be included in the onChange callback (see below)
    * `startDate` - The date that you want to be pre-selected in a single date or range. The default is today.
    * `isRange` - If you want the DatePicker to act as a range picker, include this boolean prop (see below).
    * `endDate` - If the component is a range picker, this is the end date you want to be pre-selected. The default is tomorrow.
    * `minDate` - Optional minimum threshold for only allowing dates from this date or after.
    * `maxDate` - Optional maximum threshold for only allowing dates from this date or before.
    * `dateFormat` - What is displayed on the button, as per the [date-fns format](https://date-fns.org/v1.29.0/docs/format) documentation. The default is `YYYY-MM-DD`.
    * `className` - A custom className for styling.

    ```javascript
    const onChange = ({name, startDate, endDate}) => {
    console.log({name, startDate, endDate});
    };

    <DatePicker
    name="availabilityWindow"
    startDate={new Date(2018, 2, 5)} // March 5th, 2018
    endDate={new Date(2018, 3, 12)} // April 12th, 2018
    minDate={new Date(2018, 1, 1)} // Feb 1st, 2018
    maxDate={new Date(2018, 4, 31)} // May 31st, 2018
    onChange={onChange}
    isRange
    />
  7. stevensacks revised this gist Aug 18, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion DatePicker.js
    Original file line number Diff line number Diff line change
    @@ -22,7 +22,7 @@ export default class DatePicker extends Component {

    static defaultProps = {
    dateFormat: 'YYYY-MM-DD',
    startDate: startOfDay(new Date()),
    startDate: new Date(),
    endDate: addDays(new Date(), 1),
    };

  8. stevensacks revised this gist Aug 18, 2018. 1 changed file with 358 additions and 0 deletions.
    358 changes: 358 additions & 0 deletions index.scss
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,358 @@
    @import 'css/vars.scss';
    @import 'bulma/sass/utilities/functions';
    @import 'bulma/sass/utilities/derived-variables';

    // scss-lint:disable NameFormat

    .date-picker {
    position: relative;

    .button {
    &.date-picker-button {
    position: relative;

    .icon {
    position: relative;
    top: -1px;
    }

    padding-top: 0.375em;
    padding-bottom: calc(0.375em - 2px);
    }
    }

    &-dialog {
    header,
    footer {
    display: none;
    }

    .modal-card {
    display: block;
    width: auto;
    }

    .modal-card-body {
    flex: none;
    padding: 0;
    }

    &-range {
    footer {
    display: flex;
    padding: 0.5em;
    }
    }
    }
    }

    $calendar-border: none !default;
    $calendar-border-radius: $radius-small !default;
    $calendar-header-background-color: $primary !default;
    $calendar-days-background-color: transparent !default;
    $calendar-header-days-color: $grey-light !default;
    $calendar-date-color: $text !default;
    $calendar-date-hover-background-color: $white-ter !default;
    $calendar-today-background: transparent !default;
    $calendar-today-border-color: $primary !default;
    $calendar-today-color: $primary !default;
    $calendar-range-background-color: lighten($primary, 50%) !default;
    $calendar-body-padding: 0 1em 1em 1em !default;
    $calendar-header-padding: 1em 1em 0 1em !default;
    $calendar-header-nav-padding: 0.5em !default;
    $calendar-date-padding: 0.4rem 0 !default;

    .calendar {
    position: relative;
    display: block;
    min-width: 20rem;
    max-width: 20rem;
    border: $calendar-border;
    border-radius: $calendar-border-radius;
    background: $white;
    text-align: center;

    &.is-active {
    display: initial;
    }

    .calendar-nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: $calendar-header-nav-padding;
    border-top-left-radius: $calendar-border-radius;
    border-top-right-radius: $calendar-border-radius;
    background: $calendar-header-background-color;
    color: $white;
    font-size: $size-5;

    .calendar-nav-month,
    .calendar-nav-day,
    .calendar-nav-year {
    display: flex;
    justify-content: space-between;
    align-items: center;
    width: 100%;
    }

    .calendar-month,
    .calendar-day,
    .calendar-year {
    flex: 1;
    }

    .calendar-month {
    font-size: $size-4;
    user-select: none;
    }

    .calendar-day {
    font-size: $size-2;
    }

    .calendar-nav-prev-month,
    .calendar-nav-next-month,
    .calendar-nav-prev-year,
    .calendar-nav-next-year {
    flex-basis: auto;
    flex-grow: 0;
    flex-shrink: 0;
    color: $white;
    text-decoration: none;

    &:hover {
    background-color: transparent;

    svg {
    stroke-width: 1em;
    }
    }

    svg {
    stroke: currentColor;
    width: 11.25px;
    height: 18px;
    }
    }

    &-range {
    justify-content: center;
    margin-top: -5px;
    padding: 0 0.5em;

    .icon {
    margin: 0 5px;
    }
    }
    }

    .calendar-header,
    .calendar-body {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    }

    .calendar-header .calendar-date,
    .calendar-body .calendar-date {
    flex: 0 0 14.28%;
    max-width: 14.28%;
    }

    .calendar-header {
    padding: $calendar-header-padding;
    background: $calendar-days-background-color;
    color: findColorInvert($calendar-days-background-color);
    font-size: $size-7;
    user-select: none;

    .calendar-date {
    color: $calendar-header-days-color;
    }
    }

    .calendar-body {
    padding: $calendar-body-padding;
    color: $grey;
    }

    .calendar-date {
    padding: $calendar-date-padding;
    border: 0;
    user-select: none;

    .date-item {
    position: relative;
    padding: 0.3rem;
    width: 2.2rem;
    height: 2.2rem;
    outline: none;
    border: 0.1rem solid transparent;
    border-radius: 100%;
    background: transparent;
    color: $calendar-date-color;
    vertical-align: middle;
    text-align: center;
    text-decoration: none;
    white-space: nowrap;
    line-height: 1.4rem;
    cursor: pointer;
    transition: all 0.2s ease;
    appearance: none;

    &.is-today {
    border-color: $calendar-today-border-color;
    background: $calendar-today-background;
    color: $calendar-today-color;
    }

    &:focus {
    border-color: $calendar-date-hover-background-color;
    background: $calendar-date-hover-background-color;
    color: findColorInvert($calendar-date-hover-background-color);
    text-decoration: none;
    }

    &:hover {
    border-color: $calendar-date-hover-background-color;
    background: $calendar-date-hover-background-color;
    color: findColorInvert($calendar-date-hover-background-color);
    text-decoration: none;
    }

    &.is-active {
    border-color: $primary;
    background: $primary;
    color: findColorInvert($primary);
    }
    }

    &.is-disabled {
    .date-item,
    .calendar-event {
    opacity: 0.1;
    cursor: default;
    pointer-events: none;
    }
    }

    &.is-outside-month {
    &:not(.calendar-range) {
    .date-item {
    opacity: 0.4;
    }
    }

    &.is-disabled {
    .date-item {
    opacity: 0.1;
    }
    }
    }
    }

    .calendar-range {
    position: relative;

    &::before {
    position: absolute;
    top: 50%;
    right: 0;
    left: 0;
    height: 2.2rem;
    background: $calendar-range-background-color;
    content: '';
    transform: translateY(-50%);
    }

    &.calendar-range-start::before {
    left: 50%;
    }

    &.calendar-range-end::before {
    right: 50%;
    }

    .date-item {
    color: $primary;
    }
    }

    &.is-large {
    max-width: 100%;

    .calendar-body {
    .calendar-date {
    display: flex;
    flex-direction: column;
    padding: 0;
    height: 11rem;
    border-right: $calendar-border;
    border-bottom: $calendar-border;

    &:nth-child(7n) {
    border-right: 0;
    }

    &:nth-last-child(-n + 7) {
    border-bottom: 0;
    }
    }
    }

    .date-item {
    margin-top: 0.5rem;
    margin-right: 0.5rem;
    align-self: flex-end;
    height: 2.2rem;
    }

    .calendar-range {
    &::before {
    top: 1.9rem;
    }

    &.calendar-range-start::before {
    left: auto;
    width: 1.9rem;
    }

    &.calendar-range-end::before {
    right: 1.9rem;
    }
    }

    .calendar-events {
    overflow-y: auto;
    padding: 0.5rem;
    flex-grow: 1;
    line-height: 1;
    }

    .calendar-event {
    display: block;
    overflow: hidden;
    margin: 0.2rem auto;
    padding: 0.3rem 0.4rem;
    border-radius: $radius-small;
    background-color: $grey;
    color: $white;
    vertical-align: baseline;
    text-align: left;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 1rem;

    @each $name, $pair in $colors {
    $color: nth($pair, 1);
    $color-invert: nth($pair, 2);

    &.is-#{$name} {
    background-color: $color;
    color: $color-invert;
    }
    }
    }
    }
    }
  9. stevensacks created this gist Aug 18, 2018.
    111 changes: 111 additions & 0 deletions DatePicker.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,111 @@
    import {addDays, format, isAfter, isBefore, startOfDay} from 'date-fns';
    import React, {Component, Fragment} from 'react';
    import classes from 'classnames';
    import DatePickerDialog from './DatePickerDialog';
    import {Dialog} from 'core';
    import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
    import PropTypes from 'prop-types';
    import './index.css';

    export default class DatePicker extends Component {
    static propTypes = {
    className: PropTypes.string,
    dateFormat: PropTypes.string,
    endDate: PropTypes.instanceOf(Date),
    isRange: PropTypes.bool,
    maxDate: PropTypes.instanceOf(Date),
    minDate: PropTypes.instanceOf(Date),
    name: PropTypes.string,
    onSelect: PropTypes.func.isRequired,
    startDate: PropTypes.instanceOf(Date),
    };

    static defaultProps = {
    dateFormat: 'YYYY-MM-DD',
    startDate: startOfDay(new Date()),
    endDate: addDays(new Date(), 1),
    };

    constructor(props) {
    super(props);
    const startDate =
    !props.minDate || isAfter(props.startDate, props.minDate)
    ? props.startDate
    : props.minDate;
    const endDate =
    !props.maxDate || isBefore(props.endDate, props.maxDate)
    ? props.endDate
    : props.maxDate;
    this.state = {
    startDate: startOfDay(startDate),
    endDate: props.isRange ? startOfDay(endDate) : undefined,
    };
    }

    // This is my custom Dialog opener
    // it opens a Bulma modal with a modal-card
    onClick = () =>
    Dialog({
    title: '',
    styles: {
    dialog: classes('date-picker-dialog', {
    'date-picker-dialog-range': this.props.isRange,
    }),
    },
    cancel: this.props.isRange ? 'Cancel' : undefined,
    submit: this.props.isRange ? 'Save' : undefined,
    custom: {
    View: DatePickerDialog,
    props: {
    maxDate: this.props.maxDate,
    minDate: this.props.minDate,
    isRange: this.props.isRange,
    startDate: this.state.startDate,
    endDate: this.state.endDate,
    },
    },
    }).then(this.onSelect);

    onSelect = event =>
    event &&
    this.setState(
    {startDate: event.startDate, endDate: event.endDate},
    () =>
    this.props.onSelect({
    name: this.props.name,
    startDate: this.state.startDate,
    endDate: this.state.endDate,
    })
    );

    render() {
    const {className, dateFormat, isRange} = this.props;
    const {startDate, endDate} = this.state;
    const formattedStartDate = format(startDate, dateFormat);
    const formattedEndDate = isRange ? format(endDate, dateFormat) : '';
    return (
    <div
    className={classes('date-picker', {
    [className]: !!className,
    })}
    >
    <a className="button date-picker-button" onClick={this.onClick}>
    <span className="icon">
    <FontAwesomeIcon icon={['fas', 'calendar-alt']} />
    </span>
    <span>{formattedStartDate}</span>
    {isRange && (
    <Fragment>
    <span className="icon">
    <FontAwesomeIcon
    icon={['fas', 'long-arrow-alt-right']}
    />
    </span>
    <span>{formattedEndDate}</span>
    </Fragment>
    )}
    </a>
    </div>
    );
    }
    }
    102 changes: 102 additions & 0 deletions DatePickerCalendar.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,102 @@
    import {
    addDays,
    differenceInDays,
    endOfMonth,
    endOfWeek,
    format,
    isAfter,
    isBefore,
    isEqual,
    isSameMonth,
    isToday,
    isWithinRange,
    startOfMonth,
    startOfWeek,
    } from 'date-fns';
    import DatePickerDate from './DatePickerDate';
    import PropTypes from 'prop-types';
    import React from 'react';

    const isValidDate = (date, minDate, maxDate) => {
    if (!minDate && !maxDate) return true;
    if (minDate && maxDate) return isWithinRange(date, minDate, maxDate);
    if (maxDate) return isBefore(date, maxDate) || isEqual(date, maxDate);
    return isAfter(date, minDate) || isEqual(date, minDate);
    };

    export const DatePickerCalendar = ({
    endDate,
    isRange,
    maxDate,
    minDate,
    onClickDate,
    onClickJump,
    startDate,
    visibleDate,
    }) => {
    // the 7 days of the week (Sun-Sat)
    const labels = new Array(7)
    .fill(startOfWeek(visibleDate))
    .map((d, i) => format(addDays(d, i), 'ddd'));
    // first day of current month view
    const start = startOfWeek(startOfMonth(visibleDate));
    // last day of current month view
    const end = endOfWeek(endOfMonth(visibleDate));
    // get all days and whether they are within the current month and range
    const days = new Array(differenceInDays(end, start) + 1)
    .fill(start)
    .map((s, i) => {
    const theDate = addDays(s, i);
    const isThisMonth = isSameMonth(visibleDate, theDate);
    const isInRange =
    isRange && isWithinRange(theDate, startDate, endDate);
    // if not in range, no click action
    // if in this month, select the date
    // if out of this month, jump to the date
    const onClick = !isValidDate(theDate, minDate, maxDate)
    ? null
    : isThisMonth
    ? onClickDate
    : onClickJump;
    return {
    date: theDate,
    isToday: isToday(theDate),
    isStartDate: isEqual(startDate, theDate),
    isEndDate: isEqual(endDate, theDate),
    isThisMonth,
    isInRange,
    onClick,
    };
    });
    return (
    <div className="calendar-container">
    <div className="calendar-header">
    {labels.map(day => (
    <div key={day} className="calendar-date">
    {day}
    </div>
    ))}
    </div>
    <div className="calendar-body">
    {days.map(theDate => (
    <DatePickerDate
    key={theDate.date.toString()}
    {...theDate}
    />
    ))}
    </div>
    </div>
    );
    };
    DatePickerCalendar.propTypes = {
    endDate: PropTypes.instanceOf(Date),
    isRange: PropTypes.bool,
    maxDate: PropTypes.instanceOf(Date),
    minDate: PropTypes.instanceOf(Date),
    onClickDate: PropTypes.func.isRequired,
    onClickJump: PropTypes.func.isRequired,
    startDate: PropTypes.instanceOf(Date),
    visibleDate: PropTypes.instanceOf(Date),
    };

    export default DatePickerCalendar;
    45 changes: 45 additions & 0 deletions DatePickerDate.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,45 @@
    import classes from 'classnames';
    import PropTypes from 'prop-types';
    import React from 'react';

    export const DatePickerDate = ({
    date,
    isEndDate,
    isInRange,
    isStartDate,
    isThisMonth,
    isToday,
    onClick,
    }) => (
    <div
    className={classes('calendar-date', {
    'is-outside-month': !isThisMonth,
    'is-disabled': !onClick,
    'calendar-range': isInRange,
    'calendar-range-start': isInRange && isStartDate,
    'calendar-range-end': isInRange && isEndDate,
    })}
    >
    <button
    className={classes('date-item', {
    'is-today': isToday,
    'is-active': isStartDate || isEndDate,
    })}
    onClick={onClick ? () => onClick(date) : null}
    >
    {date.getDate()}
    </button>
    </div>
    );

    DatePickerDate.propTypes = {
    date: PropTypes.instanceOf(Date).isRequired,
    isEndDate: PropTypes.bool,
    isInRange: PropTypes.bool,
    isStartDate: PropTypes.bool,
    isThisMonth: PropTypes.bool,
    isToday: PropTypes.bool,
    onClick: PropTypes.func,
    };

    export default DatePickerDate;
    160 changes: 160 additions & 0 deletions DatePickerDialog.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,160 @@
    import {
    addDays,
    addMonths,
    getDaysInMonth,
    isAfter,
    isBefore,
    isEqual,
    lastDayOfMonth,
    setDate,
    subDays,
    subMonths,
    } from 'date-fns';
    import React, {Component} from 'react';
    import DatePickerCalendar from './DatePickerCalendar';
    import DatePickerNav from './DatePickerNav';
    import PropTypes from 'prop-types';

    export default class DatePickerDialog extends Component {
    static propTypes = {
    endDate: PropTypes.instanceOf(Date),
    isRange: PropTypes.bool,
    maxDate: PropTypes.instanceOf(Date),
    minDate: PropTypes.instanceOf(Date),
    onUpdate: PropTypes.func,
    resolve: PropTypes.func,
    startDate: PropTypes.instanceOf(Date),
    };

    constructor(props) {
    super(props);
    this.state = {
    startDate: props.startDate,
    endDate: props.endDate,
    visibleDate: props.startDate,
    };
    }

    onNext = () =>
    this.setState(prevState => {
    const nextMonth = addMonths(prevState.visibleDate, 1);
    const day = Math.min(
    getDaysInMonth(nextMonth),
    prevState.visibleDate.getDate()
    );
    if (
    !this.props.maxDate ||
    isBefore(nextMonth, this.props.maxDate)
    ) {
    const visibleDate = setDate(nextMonth, day);
    return {
    visibleDate,
    };
    }
    return {
    visibleDate: this.props.maxDate,
    };
    });

    onPrev = () =>
    this.setState(prevState => {
    const prevMonth = lastDayOfMonth(
    subMonths(
    new Date(
    prevState.visibleDate.getFullYear(),
    prevState.visibleDate.getMonth()
    ),
    1
    )
    );
    const day = Math.min(
    getDaysInMonth(prevMonth),
    prevState.visibleDate.getDate()
    );
    if (!this.props.minDate || isAfter(prevMonth, this.props.minDate)) {
    const visibleDate = setDate(prevMonth, day);
    return {
    visibleDate,
    };
    }
    return {
    visibleDate: this.props.minDate,
    };
    });

    onClickJump = date => {
    if (!this.props.isRange) {
    this.props.resolve({startDate: date});
    } else {
    this.setStartAndEnd(date);
    }
    };

    onClickDate = date => {
    if (!this.props.isRange) {
    this.props.resolve({startDate: date});
    } else {
    this.setStartAndEnd(date);
    }
    };

    onClickNav = date => {
    this.setState({visibleDate: date});
    };

    setStartAndEnd = date => {
    const {startDate, endDate} = this.state;
    const newDates = {
    startDate,
    endDate,
    };
    if (isEqual(date, startDate)) {
    // reset start and end dates anchored to start
    newDates.startDate = date;
    newDates.endDate = addDays(date, 1);
    } else if (isEqual(date, endDate)) {
    // reset start and end dates anchored to end
    newDates.startDate = subDays(date, 1);
    newDates.endDate = date;
    } else if (isBefore(date, startDate)) {
    // extend the start date
    newDates.startDate = date;
    } else if (isAfter(date, endDate)) {
    // extend the end date
    newDates.endDate = date;
    } else if (isAfter(date, startDate)) {
    // truncate start date
    newDates.startDate = date;
    }
    this.setState({...newDates, visibleDate: date});
    this.props.onUpdate(newDates);
    };

    render() {
    const {isRange} = this.props;
    const {startDate, endDate, visibleDate} = this.state;
    return (
    <div className="calendar">
    <DatePickerNav
    visibleDate={visibleDate}
    startDate={startDate}
    endDate={endDate}
    isRange={isRange}
    onNext={this.onNext}
    onPrev={this.onPrev}
    onJump={this.onClickNav}
    />
    <DatePickerCalendar
    visibleDate={visibleDate}
    startDate={startDate}
    endDate={endDate}
    isRange={isRange}
    minDate={this.props.minDate}
    maxDate={this.props.maxDate}
    onClickDate={this.onClickDate}
    onClickJump={this.onClickJump}
    />
    </div>
    );
    }
    }
    67 changes: 67 additions & 0 deletions DatePickerNav.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,67 @@
    import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
    import {format} from 'date-fns';
    import PropTypes from 'prop-types';
    import React, {Fragment} from 'react';

    export const DatePickerNav = ({
    visibleDate,
    startDate,
    endDate,
    isRange,
    onJump,
    onNext,
    onPrev,
    }) => (
    <Fragment>
    <div className="calendar-nav">
    <div className="calendar-nav-prev-month" onClick={onPrev}>
    <button className="button is-primary">
    <span className="icon">
    <FontAwesomeIcon icon={['fas', 'chevron-left']} />
    </span>
    </button>
    </div>
    <div className="calendar-month">
    {format(visibleDate, 'MMMM YYYY')}
    </div>
    <div className="calendar-nav-next-month" onClick={onNext}>
    <button className="button is-primary">
    <span className="icon">
    <FontAwesomeIcon icon={['fas', 'chevron-right']} />
    </span>
    </button>
    </div>
    </div>
    {isRange && (
    <div className="calendar-nav calendar-nav-range">
    <a
    className="button is-primary"
    onClick={() => onJump(startDate)}
    >
    {format(startDate, 'YYYY-MM-DD')}
    </a>
    <span className="icon">
    <FontAwesomeIcon icon={['fas', 'long-arrow-alt-right']} />
    </span>
    <a
    className="button is-primary"
    onClick={() => onJump(endDate)}
    >
    {format(endDate, 'YYYY-MM-DD')}
    </a>
    </div>
    )}
    </Fragment>
    );

    DatePickerNav.propTypes = {
    endDate: PropTypes.instanceOf(Date),
    isRange: PropTypes.bool,
    onJump: PropTypes.func,
    onNext: PropTypes.func.isRequired,
    onPrev: PropTypes.func.isRequired,
    startDate: PropTypes.instanceOf(Date),
    visibleDate: PropTypes.instanceOf(Date),
    };

    export default DatePickerNav;