Source

src/components/Interface/Slider.jsx

import React from 'react'
import { useDispatch } from 'react-redux'
import Grid from '@mui/material/Grid'
import Slider from '@mui/material/Slider'
import Button from '@mui/material/Button'
import styled from 'styled-components'
import useTickUpdate from '../../hooks/useTickUpdate'
import colors from '../../config/colors'
import { debounce, findClosestValue } from '../../utils'
import useCurrentDateIndices from '../../hooks/useCurrentDateIndices'
import Ticks from './Ticks'
import DatePicker from 'react-date-picker'
import { StyledSlider, Icon } from '..'
import { paramsActions } from '../../stores/paramsStore'
const { setDataParams } = paramsActions
const SliderContainer = styled(Grid)`
    color: white;
    box-sizing: border-box;
    padding: 0 0.5em 0.5em 0.5em;
    width: 100%;
    user-select: none;
`
const SliderRow = styled.div`
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    gap: 0.5em;
    width: 100%;
    padding-right: 0.5em;
    p {
        flex-grow: 0;
    }
`

const PlayPauseButton = styled(Button)`
    background: none;
    flex-grow: 0;
    padding: 0;
    margin: 0 0 -0.5em 0;
    width: 100%;
    width: 3em;
    height: 3em;
    &.MuiButton-root {
        min-width: auto;
        max-width: 100%;
        span {
            width: 100%;
        }
    }
    svg {
        fill: white;
    }
`

const SliderAndTicksContainer = styled.div`
    position: relative;
    flex-grow: 1;
    margin: 0 1.5em 5px 1em;
`

const SliderAndTicksInner = styled.div``

const SpeedSlider = styled.div`
    position: absolute;
    padding: 1em 2em 0 1em !important;
    width: 15%;
    min-width: 100px;
    max-width: 150px;
    background: ${colors.gray};
    left: 0;
    top: calc(100% + 0.25em);
    /* box-shadow: 0px 0px 5px rgb(0 0 0 / 70%); */
    p {
        text-align: center;
    }
    span.MuiSlider-rail {
        display: initial !important;
    }
    span.MuiSlider-track {
        display: initial !important;
    }
    span.MuiSlider-thumbColorPrimary {
        transform: translateY(-7px) !important;
    }
`

const RangeSlider = styled(Slider)`
    box-sizing: border-box;
    &.MuiSlider-root {
        box-sizing: border-box;
        color: #ffffff55;
    }
    // span.MuiSlider-rail {
    //   color: white;
    //   height: 4px;
    //   display: none;
    // }
    span.MuiSlider-track {
        color: ${colors.yellow};
        height: 4px;
        opacity: 0.5;
        transform: translateY(1px);
    }
    span.MuiSlider-thumb {
        color: white;
        width: 15px;
        height: 15px;
        transform: translate(-1.5px, -4px);
        border: 2px solid ${colors.gray};
        .MuiSlider-valueLabel {
            transform: translateY(-10px);
            pointer-events: none;
            font-size: 15px;
            span {
                background: none;
            }
        }
    }
    span.MuiSlider-mark {
        width: 1px;
        height: 2px;
    }
    span.muislider-thumb.muislider-active:hover {
        box-shadow: 0px 0px 10px rgba(200, 200, 200, 0.5);
    }
`

const DateSelectorContainer = styled(Grid)`
    display: flex;
    justify-items: center;
    justify-content: center;
    align-items: center;
    margin: 0;
    padding: 0;
    .MuiFormControl-root {
        padding: 0 0 0 20px !important;
    }
    span {
        font-weight: bold;
    }
    #dateSelector {
        position: absolute;
        left: 50%;
        transform: translateX(-50%);
        @media (max-width: 600px) {
            transform: none;
            left: 20px;
        }
        @media (max-width: 450px) {
            transform: none;
            left: 0px;
        }
    }
    .react-date-picker {
        /* display: block;
    margin:0 auto; */
        color: white;
        padding: 0.5em 0 0 0;
    }
    .react-date-picker__inputGroup__input,
    span {
        color: white;
        font-size: 1rem;
        font-weight: bold;
        font-family: 'Lato', sans-serif;
    }
    .react-date-picker__clear-button {
        display: none;
    }
    svg rect,
    svg line {
        stroke: white;
    }
    .react-calendar {
        background: ${colors.darkgray};
        transform: translate(-33.33%, 0.5em);
        button {
            color: white;
            background: none;
            transition: 250ms all;
            &:disabled {
                opacity: 0.05;
            }
            &:hover {
                background: none;
                color: ${colors.yellow};
            }
            &.react-calendar__month-view__days__day--neighboringMonth {
                opacity: 0.5;
                &:disabled {
                    opacity: 0.05;
                }
            }
        }
    }
    p {
        padding: 4px 1em 0 1em;
        font-size: 1rem;
    }
`

const valuetext = (dates, value) => {
    const fullDate = dates[value]?.split('-')

    return fullDate && `${parseInt(fullDate[1])}/${fullDate[0]?.slice(2)}`
}

const speedtext = (value) => `Animation Tick Rate: ${value} milliseconds`

function DateTitle({
    dates = [],
    currDatesAvailable = [],
    currIndex = 7,
    currRange = 7,
    rangeType = '',
    handleChange = () => {},
    handleRangeChange = () => {},
}) {
    if (!dates || !dates.length) {
        return null
    }

    const currStartDate = new Date(dates[currIndex - currRange + 1] || '2020-01-01')
    const firstDateIdx = currDatesAvailable.indexOf(1)
    const lastDateIdx = [...currDatesAvailable].reverse().indexOf(1)
    const minDate = new Date(dates[firstDateIdx] || '2020-01-01')
    const maxDate = new Date(dates.slice(-lastDateIdx)[0] || '')
    const currDate = new Date(dates[currIndex+1] || '2020-01-01')

    const onChange =
        rangeType === 'custom'
            ? (date, position) => {
                  try {
                      const dateString = JSON.stringify(date).slice(1, 11)
                      const dateIdx = dates.indexOf(dateString)
                      if (position === 'start') {
                          handleRangeChange(null, [dateIdx, currIndex])
                      } else {
                          handleRangeChange(null, [
                              currIndex - currRange,
                              dateIdx,
                          ])
                      }
                  } catch (error) {
                      console.log(error)
                  }
              }
            : (date) => {
                  try {
                      const dateString = JSON.stringify(date).slice(1, 11)
                      const dateIdx = dates.indexOf(dateString)
                      handleChange(null, dateIdx)
                  } catch (error) {
                      console.log(error)
                  }
              }
    return (
        <DateSelectorContainer item xs={12}>
            {rangeType === 'custom' && (
                <DatePicker
                    calendarAriaLabel="Toggle calendar"
                    clearAriaLabel="Clear value"
                    dayAriaLabel="Day"
                    monthAriaLabel="Month"
                    minDate={minDate}
                    maxDate={maxDate}
                    nativeInputAriaLabel="Date"
                    onChange={onChange}
                    value={currStartDate}
                    yearAriaLabel="Year"
                />
            )}

            {rangeType === 'custom' && <p>to</p>}
            <DatePicker
                calendarAriaLabel="Toggle calendar"
                clearAriaLabel="Clear value"
                dayAriaLabel="Day"
                monthAriaLabel="Month"
                minDate={minDate}
                maxDate={maxDate}
                nativeInputAriaLabel="Date"
                onChange={onChange}
                value={currDate}
                yearAriaLabel="Year"
            />
            {/* <DateH3>
        {rangeType === 'custom' && dates && dates.length && currIndex-currRange !== undefined ? `${formatDate(dates[currIndex-currRange])} to ` : ""}
        {(dates && dates.length && currIndex !== undefined) ? formatDate(dates[currIndex]) : ""}
      </DateH3> */}
        </DateSelectorContainer>
    )
}

const findLastDate = (array) => {
    for (let i = array.length - 1; i >= 0; i--) {
        if (array[i] === 1) {
            return i
        }
    }
}
/**
 * Standalone stateful component for displaying a slider with a single date or
 * date range. Also controls animation and playback (managed in
 * src/Hooks/useTickUpdate)
 *
 * @category Components/Interface
 * @component
 */
function DateSlider() {
    const dispatch = useDispatch()
    const [
        currIndex,
        currDates,
        currDatesAvailable,
        allDates,
        currRange,
        rangeType,
    ] = useCurrentDateIndices()

    const [isTicking, setIsTicking, timing, setTiming] = useTickUpdate({
        currDatesAvailable,
    })

    const handleChange = debounce((_, newValue) => {
        // eslint-disable-line
        if (currDatesAvailable[newValue]) {
            dispatch(setDataParams({ nIndex: newValue }))
        } else {
            dispatch(
                setDataParams({
                    nIndex: findClosestValue(
                        newValue,
                        currDatesAvailable,
                        newValue < currIndex
                    ),
                })
            )
        }
    }, 5)

    const handleRangeChange = debounce((_, newValue) => {
        const newIndex = Math.max(newValue[0], newValue[1])
        const newRange = newIndex - Math.min(newValue[0], newValue[1]) // eslint-disable-line
        if (currDatesAvailable[newIndex]) {
            dispatch(setDataParams({ nIndex: newIndex, nRange: newRange }))
        } else {
            dispatch(
                setDataParams({
                    nIndex: findClosestValue(
                        newIndex,
                        currDatesAvailable,
                        newIndex < currIndex
                    ),
                    nRange: newRange,
                })
            )
        }
    }, 25)

    const handlePlayPause = () => {
        if (!isTicking) {
            setIsTicking(true)
        } else {
            setIsTicking(false)
        }
    }

    const shouldShowLineSlider = rangeType !== 'custom'
    const shouldShowRangeSlider = rangeType === 'custom'
    return (
        <SliderContainer container spacing={0}>
            <DateTitle
                currIndex={currIndex}
                currRange={currRange}
                rangeType={rangeType}
                dates={allDates}
                handleChange={handleChange}
                handleRangeChange={handleRangeChange}
                currDatesAvailable={currDatesAvailable}
            />
            <SliderRow>
                <PlayPauseButton id="playPause" onClick={handlePlayPause}>
                    <Icon symbol={isTicking ? 'pause' : 'play'} />
                </PlayPauseButton>
                <p>{valuetext(allDates, 0)}</p>
                <SliderAndTicksContainer>
                    <SliderAndTicksInner>
                        <Ticks
                            id="ticks"
                            loaded={currDates}
                            available={currDatesAvailable}
                            fullLength={allDates.length}
                        />
                    </SliderAndTicksInner>
                    <SliderAndTicksInner>
                        {shouldShowLineSlider && (
                            <StyledSlider
                                id="timeSlider"
                                value={currIndex}
                                // valueLabelDisplay="on"
                                onChange={handleChange}
                                getAriaValueText={valuetext}
                                valueLabelFormat={valuetext}
                                aria-labelledby="aria-valuetext"
                                min={0}
                                max={allDates.length}
                                step={1}
                            />
                        )}
                        {shouldShowRangeSlider && (
                            <RangeSlider
                                id="timeSlider"
                                value={[currIndex - currRange, currIndex]}
                                onChange={handleRangeChange}
                                getAriaValueText={valuetext}
                                valueLabelFormat={valuetext}
                                aria-labelledby="aria-valuetext"
                                min={0}
                                max={allDates.length}
                                step={1}
                            />
                        )}
                    </SliderAndTicksInner>
                </SliderAndTicksContainer>
                <p>{valuetext(allDates, findLastDate(currDatesAvailable))}</p>
                {!!isTicking && (
                    <SpeedSlider>
                        <p>Animation Speed</p>
                        <StyledSlider
                            value={1000 - timing}
                            onChange={(e, newValue) =>
                                setTiming(1000 - newValue)
                            }
                            getAriaValueText={speedtext}
                            valueLabelFormat={speedtext}
                            aria-labelledby="aria-valuetext"
                            min={25}
                            max={975}
                            step={25}
                        />
                    </SpeedSlider>
                )}
            </SliderRow>
        </SliderContainer>
    )
}

export default React.memo(DateSlider)