diff --git a/package.json b/package.json index ef2abc5..227292c 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,10 @@ "internal-nav-helper": "^3.1.0", "money-clip": "^3.0.5", "react": "^18.2.0", + "react-calendar": "^5.1.0", "react-colorful": "^5.5.1", - "react-datepicker": "^4.7.0", + "react-date-picker": "^11.0.0", + "react-datepicker": "^4.25.0", "react-dom": "^18.2.0", "react-select": "^5.2.2", "redux-bundler": "^28.0.3", diff --git a/src/app-bundles/product-bundle.js b/src/app-bundles/product-bundle.js index a3ff470..21ae024 100644 --- a/src/app-bundles/product-bundle.js +++ b/src/app-bundles/product-bundle.js @@ -3,6 +3,8 @@ import { createSelector } from 'redux-bundler'; import { getMonth, getYear, isAfter, isBefore } from 'date-fns'; const apiUrl = process.env.REACT_APP_CUMULUS_API_URL; +const cumulusBegin = new Date('01-Jan-1980'); +const MAX_DATE = new Date('01-Jan-3000'); export default createRestBundle({ name: 'product', @@ -114,13 +116,16 @@ export default createRestBundle({ selectProductDateRangeFrom: createSelector( 'selectProductItemsArray', (items) => { - let out = new Date('01-Jan-3000'); + let out = MAX_DATE; items.forEach((p) => { if (p.productfile_count > 0) { const d = new Date(p.after); if (isBefore(d, out)) out = d; } }); + if (out.getTime() === MAX_DATE.getTime()) { + out = cumulusBegin; + } return out; } ), @@ -128,13 +133,16 @@ export default createRestBundle({ selectProductDateRangeTo: createSelector( 'selectProductItemsArray', (items) => { - let out = new Date('01-Jan-1980'); + let out = cumulusBegin; items.forEach((p) => { if (p.productfile_count > 0) { const d = new Date(p.before); if (isAfter(d, out)) out = d; } }); + if (out.getTime() === cumulusBegin.getTime()) { + out = new Date(); + } return out; } ), diff --git a/src/app-pages/products/DatePickerOverrides.css b/src/app-pages/products/DatePickerOverrides.css new file mode 100644 index 0000000..af2fae3 --- /dev/null +++ b/src/app-pages/products/DatePickerOverrides.css @@ -0,0 +1,114 @@ +/* src/DatePickerOverrides.css */ + +.calendar-only .react-date-picker__inputGroup:hover { + color: #0078d4; +} +.calendar-only .react-date-picker__calendar-button:hover { + color: #0078d4; +} + +.calendar-only .react-date-picker__clear-button { + display: none; +} +.calendar-only .react-date-picker__wrapper--with-border { + border: 1px solid #b3b3b3; + border-radius: 5px; + padding: 0; + margin: 3px; +} + +.calendar-only .react-date-picker__wrapper{ + border: none; + padding: 0; +} + +.my-picker-wrapper .react-calendar { + border: none; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); + overflow: hidden; + font-family: 'Segoe UI', Roboto, sans-serif; + background-color: #ffffff; +} + +.my-picker-wrapper .react-calendar__navigation { + display: flex; + justify-content: space-between; + align-items: center; + background-color: #f5f5f5; + padding: 10px; + border-bottom: 1px solid #e0e0e0; + margin-bottom: 0; /* Ensure no gap between navigation and weekdays */ +} + +.my-picker-wrapper .react-calendar__navigation button { + background: none; + border: none; + color: #333; + font-size: 16px; + cursor: pointer; + padding: 5px 10px; + border-radius: 4px; + transition: background-color 0.2s ease; +} + +.my-picker-wrapper .react-calendar__navigation button:hover { + background-color: #e0e0e0; + color: #0078d4; + outline: 2px solid #0078d4; + outline-offset: -2px; + fill: #0078d4; +} + +.my-picker-wrapper .react-calendar__month-view__weekdays { + text-align: center; + font-weight: bold; + color: #666; + background-color: #f9f9f9; + padding: 10px 0; + border-bottom: 1px solid #e0e0e0; + text-underline-offset: 100px; +} + +.my-picker-wrapper .react-calendar__tile { + text-align: center; + padding: 10px; + background: none; + border: none; + font-size: 14px; + color: #333; + border-radius: 4px; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.my-picker-wrapper .react-calendar__tile:hover { + background-color: #e0e0e0; + color: #0078d4; + outline: 2px solid #0078d4; + outline-offset: -3px; +} + +.my-picker-wrapper .react-calendar__tile--active { + background-color: #e0e0e0; + color: #0078d4; + outline: 2px solid #0078d4; + outline-offset: -3px; + font-weight: bold; +} + +.my-picker-wrapper .react-calendar__tile--now { + background-color: #f0f8ff; + color: #0078d4; + font-weight: bold; +} + +.my-picker-wrapper .react-calendar__tile--now:hover { + background-color: #d0e8ff; + color: #005a9e; +} + +.my-picker-wrapper .react-calendar__tile--disabled { + color: #ccc; + background-color: #f9f9f9; + cursor: not-allowed; +} \ No newline at end of file diff --git a/src/app-pages/products/date-range-slider.js b/src/app-pages/products/date-range-slider.js index 0366f81..7ce5d4f 100644 --- a/src/app-pages/products/date-range-slider.js +++ b/src/app-pages/products/date-range-slider.js @@ -1,13 +1,24 @@ import { - format, fromUnixTime, getUnixTime, milliseconds, millisecondsToSeconds, minutesToHours, secondsToMinutes, + startOfDay, } from 'date-fns'; +import { + ChevronDoubleLeftIcon, + ChevronDoubleRightIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from '@heroicons/react/24/outline'; +import { CalendarIcon } from '@heroicons/react/24/solid'; import { useState, useRef, useEffect } from 'react'; +import DatePicker from 'react-date-picker'; +import 'react-date-picker/dist/DatePicker.css'; +import 'react-calendar/dist/Calendar.css'; +import './DatePickerOverrides.css'; // convert a value from a duration object r.e. https://date-fns.org/v2.28.0/docs/Duration // to an object with the equivalent durations in all units @@ -30,16 +41,16 @@ export default function DateRangeSlider({ stepDuration = { days: 1 }, onChange, }) { - const min = getUnixTime(minDate); - const max = getUnixTime(maxDate); + const min = getUnixTime(startOfDay(minDate)); + const max = getUnixTime(startOfDay(maxDate)); const [from, setFrom] = useState(min); const [to, setTo] = useState(max); const step = duration_to_all_units(stepDuration)['seconds']; useEffect(() => { - setFrom(min); - setTo(max); - }, [min, max]); + setFrom(getUnixTime(startOfDay(minDate))); + setTo(getUnixTime(startOfDay(maxDate))); + }, [minDate, maxDate]); useEffect(() => { onChange({ from: fromUnixTime(from), to: fromUnixTime(to) }); @@ -48,56 +59,55 @@ export default function DateRangeSlider({ const barRef = useRef(); const labelRefFrom = useRef(); const labelRefTo = useRef(); + const [barWidth, setBarWidth] = useState( + barRef.current ? barRef.current.offsetWidth : 0 + ); + const [labelWidthFrom, setLabelWidthFrom] = useState( + labelRefFrom.current ? labelRefFrom.current.offsetWidth : 0 + ); + const [labelWidthTo, setLabelWidthTo] = useState( + labelRefTo.current ? labelRefTo.current.offsetWidth : 0 + ); - let barWidth = 0; - if (barRef.current) { - barWidth = barRef.current.offsetWidth; - } + const posPx = (value, min, max, containerPx, subtractPx = 0, addPx = 0) => + ((value - min) / (max - min)) * (containerPx - subtractPx) + addPx; - let labelWidthFrom = 0; - if (labelRefFrom.current) { - labelWidthFrom = labelRefFrom.current.offsetWidth; - } + const [offsetFrom, setOffsetFrom] = useState( + posPx(from, min, max, barWidth, labelWidthFrom) + ); + const [offsetTo, setOffsetTo] = useState( + posPx(to, min, max, barWidth, labelWidthTo) + ); + const [rangeBarFrom, setRangeBarFrom] = useState( + posPx(from, min, max, barWidth, 14, 7) + ); + const [rangeBarTo, setRangeBarTo] = useState( + posPx(to, min, max, barWidth, 14, 7) + ); - let labelWidthTo = 0; - if (labelRefTo.current) { - labelWidthTo = labelRefTo.current.offsetWidth; - } + useEffect(() => { + if (!barRef.current) return; - const offsetFrom = ((from - min) / (max - min)) * (barWidth - labelWidthFrom); - const offsetTo = ((to - min) / (max - min)) * (barWidth - labelWidthTo); + const resizeObserver = new ResizeObserver(() => { + setBarWidth(barRef.current.offsetWidth); + setLabelWidthFrom(labelRefFrom.current.offsetWidth); + setLabelWidthTo(labelRefTo.current.offsetWidth); + }); - const rangeBarFrom = ((from - min) / (max - min)) * (barWidth - 12) + 6; - const rangeBarTo = ((to - min) / (max - min)) * (barWidth - 12) + 6; + resizeObserver.observe(barRef.current); - // handle range drag - const [firstClientX, setfirstClientX] = useState(null); - const handleRangeDrag = (e) => { - if (!firstClientX) { - setfirstClientX(e.clientX); - } else { - // time increment per pixel - const ratio = (max - min) / barWidth; - // change in pixels - const deltaX = e.clientX - firstClientX; - // change in time increment - const deltaT = Math.round(deltaX * ratio); - console.log(duration_to_all_units({ seconds: deltaT })); - // new times - const newFrom = from + deltaT; - const newTo = to + deltaT; - if (newFrom >= min && newTo <= max) { - setFrom(newFrom); - setTo(newTo); - } else { - console.log(Math.round(newFrom) - min, max - Math.round(newTo)); - } - } - }; + // Cleanup observer on component unmount + return () => { + resizeObserver.disconnect(); + }; + }, []); - const handleDragEnd = (e) => { - setfirstClientX(null); - }; + useEffect(() => { + setOffsetFrom(posPx(from, min, max, barWidth, labelWidthFrom)); + setOffsetTo(posPx(to, min, max, barWidth, labelWidthTo)); + setRangeBarFrom(posPx(from, min, max, barWidth, 14, 7)); + setRangeBarTo(posPx(to, min, max, barWidth, 14, 7)); + }, [barWidth, labelWidthFrom, labelWidthTo, min, max, from, to]); return (