complete rework

This commit is contained in:
Bernt Christian Egeland 2023-01-21 23:11:59 +01:00
parent cd39b0ca7a
commit 216194bfbd

View File

@ -1,90 +1,170 @@
import { h } from 'preact';
import { useState } from 'preact/hooks';
import { useEffect, useState } from 'preact/hooks';
import { ArrowDropdown } from '../icons/ArrowDropdown';
import { ArrowDropup } from '../icons/ArrowDropup';
const CalendarTimePicker = ({ dateRange, onChange }) => {
const [selected, setSelected] = useState([0]);
const TimePicker = ({ dateRange, onChange }) => {
const [error, setError] = useState(null);
const [timeRange, setTimeRange] = useState(new Set());
const timestampHour = (timestamp) => {
if (!timestamp) return undefined;
return new Date(timestamp).getHours();
useEffect(() => {
if (!dateRange.after) return setTimeRange(new Set());
}, [dateRange.after]);
// We subtract one from the before time to count the exact number of days in unix timestamp selected in the calendar
const before = dateRange.before ? new Date(dateRange.before) : new Date(new Date().setHours(24, 0, 0, 0));
const after = dateRange.after ? new Date(dateRange.after) : new Date(new Date().setHours(0, 0, 0, 0));
/**
numberOfDaysSelected is a set that holds the number of days selected in the dateRange.
The loop iterates through the days starting from the after date's day to the before date's day.
If the before date's hour is 0, it skips it.
*/
const numberOfDaysSelected = new Set(
[...Array(before.getDate() - after.getDate() + 1)].map((_, i) => after.getDate() + i)
);
if (before.getHours() === 0) numberOfDaysSelected.delete(before.getDate());
// Create repeating array with the number of hours for each day selected ...23,24,0,1,2...
const hoursInDays = Array.from({ length: numberOfDaysSelected.size * 24 }, (_, i) => i % 24);
const handleTime = (hour) => {
if (!hour) return;
const _timeRange = new Set([...timeRange]);
_timeRange.add(hour);
setError(null);
if (timeRange.has(hour)) {
setTimeRange(new Set());
const resetBefore = before.setDate(after.getDate() + numberOfDaysSelected.size - 1);
return onChange({
after: after.setHours(0, 0, 0, 0) / 1000,
before: new Date(resetBefore).setHours(24, 0, 0, 0) / 1000,
});
}
//update after
if (_timeRange.size === 1) {
// check if the first selected value is within first day
const selDay = Math.ceil(Math.min(..._timeRange) / 24);
if (selDay !== 1) {
return setError('Select a time on the initial day!');
}
// calculate days offset
const dayOffsetAfter = new Date(after).setHours(Math.min(..._timeRange));
let dayOffsetBefore = before;
if (numberOfDaysSelected.size === 1) {
dayOffsetBefore = new Date(after).setHours(Math.min(..._timeRange) + 1);
}
onChange({
after: dayOffsetAfter / 1000,
before: dayOffsetBefore / 1000,
});
}
//update before
if (_timeRange.size > 1) {
let selectedDay = Math.ceil(Math.max(..._timeRange) / 24);
// if user selects time 00:00 for the next day, add one day
if (hour === 24 && selectedDay === numberOfDaysSelected.size - 1) {
selectedDay += 1;
}
// Check if end time is on the last day
if (selectedDay !== numberOfDaysSelected.size) {
return setError('Ending must occur on final day!');
}
// Check if end time is later than start time
const startHour = Math.min(..._timeRange);
if (hour <= startHour) {
return setError('Ending hour must be greater than start time!');
}
// Add all hours between start and end times to the set
for (let x = startHour; x <= hour; x++) {
_timeRange.add(x);
}
// calculate days offset
const dayOffsetBefore = new Date(dateRange.after);
onChange({
after: dateRange.after / 1000,
// we add one hour to get full 60min of last selected hour
before: dayOffsetBefore.setHours(Math.max(..._timeRange) + 1) / 1000,
});
}
for (let i = 0; i < _timeRange.size; i++) {
setTimeRange((timeRange) => timeRange.add(Array.from(_timeRange)[i]));
}
};
const handleTime = (number) => {
const currentHour = new Date();
currentHour.setHours(number, 0, 0, 0);
// Get current time if dateRange.after or dateRange.before are not set
const afterTime = dateRange.after ? dateRange.after : currentHour.getTime();
const beforetime = afterTime + 1;
// Update dateRange object with new "before" time and set selected to new number
if (selected.length === 1 && number > Math.max(...selected)) {
onChange({
after: afterTime / 1000,
before: new Date(afterTime).setHours(number + 1) / 1000,
});
return setSelected([number]);
}
// Updating the start of the selected range
if (selected.length > 0 && number < selected[0]) {
onChange({
after: new Date(afterTime).setHours(number) / 1000,
before: dateRange.before / 1000,
});
return setSelected([number]);
}
// Updating the end of the selected range
if (selected.length === 2) {
onChange({
after: dateRange.after ? dateRange.after / 1000 : 0,
before: new Date(beforetime) / 1000,
});
return setSelected([number]);
}
// Update dateRange object with both "after" and "before" times
if (selected.length === 1) {
onChange({
after: new Date(afterTime).setHours(number) / 1000,
before: new Date(afterTime).setHours(number + 1) / 1000,
});
return setSelected([Math.min(selected[0], number), Math.max(selected[0], number)]);
}
setSelected([number]);
};
const isSelected = (after, before, idx) => {
if (timestampHour(before) === 0 && timestampHour(after) === 0) return false;
return timestampHour(after) <= idx && timestampHour(before - 1) >= idx;
return !!timeRange.has(idx);
};
const isSelectedCss = 'bg-blue-600 transition-all duration-50 hover:rounded-none';
function randomGrayTone(shade) {
const grayTones = [
'bg-[#212529]/50',
'bg-[#343a40]/50',
'bg-[#495057]/50',
'bg-[#666463]/50',
'bg-[#817D7C]/50',
'bg-[#73706F]/50',
'bg-[#585655]/50',
'bg-[#4F4D4D]/50',
'bg-[#454343]/50',
'bg-[#363434]/50',
];
return grayTones[shade % grayTones.length];
}
return (
<div aria-label="Calendar timepicker, select a time range">
<p className="block text-center font-medium mb-2 text-gray-300">TimePicker</p>
<div className="w-24 px-1">
<div
className="border border-gray-400 cursor-pointer hide-scroll"
style={{ maxHeight: '23rem', overflowY: 'scroll' }}
>
{Array.from({ length: 24 }, (_, idx) => (
<div
key={idx}
className={`w-full font-light border border-transparent hover:border-gray-300 rounded-sm text-center text-lg
${isSelected(dateRange.after, dateRange.before, idx) ? 'bg-blue-500' : ''}`}
onClick={() => handleTime(idx)}
>
<span aria-label={`${idx}:00`}>{idx}:00</span>
</div>
))}
<>
{error ? <span className="text-red-400 text-center text-xs absolute top-1 right-0 pr-2">{error}</span> : null}
<div className="mt-2 pr-5 hidden xs:block" aria-label="Calendar timepicker, select a time range">
<div className="flex items-center justify-center">
<ArrowDropup className="w-10 text-center" />
</div>
<div className="w-24 px-1">
<div
className="border border-gray-400/50 cursor-pointer hide-scroll shadow-md rounded-md"
style={{ maxHeight: '17rem', overflowY: 'scroll' }}
>
{hoursInDays.map((_, idx) => (
<div
key={idx}
className={`${isSelected(dateRange.after, dateRange.before, idx) ? isSelectedCss : ''}
${Math.min(...timeRange) === idx ? 'rounded-t-lg' : ''}
${Math.max(...timeRange) === idx ? 'rounded-b-lg' : ''}`}
>
<div
className={`
text-gray-300 w-full font-light border border-transparent hover:border hover:rounded-md hover:border-gray-600 text-center text-sm
${randomGrayTone([Math.floor(idx / 24)])}`}
onClick={() => handleTime(idx)}
>
<span aria-label={`${idx}:00`}>{hoursInDays[idx]}:00</span>
</div>
</div>
))}
</div>
<div className="flex items-center justify-center">
<ArrowDropdown className="w-10 text-center" />
</div>
</div>
</div>
</div>
</>
);
};
export default CalendarTimePicker;
export default TimePicker;