mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-02 01:05:20 +03:00
new datepicker
This commit is contained in:
parent
6e6ee93282
commit
8495bc7a2d
297
web/src/components/Calender.jsx
Normal file
297
web/src/components/Calender.jsx
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { useEffect, useState, useRef } from 'preact/hooks';
|
||||||
|
import ArrowRight from '../icons/ArrowRight';
|
||||||
|
import ArrowRightDouble from '../icons/ArrowRightDouble';
|
||||||
|
|
||||||
|
let oneDay = 60 * 60 * 24 * 1000;
|
||||||
|
let todayTimestamp = Date.now() - (Date.now() % oneDay) + new Date().getTimezoneOffset() * 1000 * 60;
|
||||||
|
|
||||||
|
const Calender = ({ onChange, calenderRef }) => {
|
||||||
|
const inputRef = useRef();
|
||||||
|
// const inputRefs = useRef();
|
||||||
|
let date = new Date();
|
||||||
|
let year = date.getFullYear();
|
||||||
|
let month = date.getMonth();
|
||||||
|
|
||||||
|
const [state, setState] = useState({
|
||||||
|
getMonthDetails: [],
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
selectedDay: null,
|
||||||
|
selectedRange: { before: null, after: null },
|
||||||
|
selectedRangeBefore: false,
|
||||||
|
monthDetails: null,
|
||||||
|
});
|
||||||
|
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setState((prev) => ({ ...prev, selectedDay: todayTimestamp, monthDetails: getMonthDetails(year, month) }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const daysMap = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||||
|
const monthMap = [
|
||||||
|
'January',
|
||||||
|
'February',
|
||||||
|
'March',
|
||||||
|
'April',
|
||||||
|
'May',
|
||||||
|
'June',
|
||||||
|
'July',
|
||||||
|
'August',
|
||||||
|
'September',
|
||||||
|
'October',
|
||||||
|
'November',
|
||||||
|
'December',
|
||||||
|
];
|
||||||
|
|
||||||
|
const getDayDetails = (args) => {
|
||||||
|
let date = args.index - args.firstDay;
|
||||||
|
let day = args.index % 7;
|
||||||
|
let prevMonth = args.month - 1;
|
||||||
|
let prevYear = args.year;
|
||||||
|
if (prevMonth < 0) {
|
||||||
|
prevMonth = 11;
|
||||||
|
prevYear--;
|
||||||
|
}
|
||||||
|
let prevMonthNumberOfDays = getNumberOfDays(prevYear, prevMonth);
|
||||||
|
let _date = (date < 0 ? prevMonthNumberOfDays + date : date % args.numberOfDays) + 1;
|
||||||
|
let month = date < 0 ? -1 : date >= args.numberOfDays ? 1 : 0;
|
||||||
|
let timestamp = new Date(args.year, args.month, _date).getTime();
|
||||||
|
return {
|
||||||
|
date: _date,
|
||||||
|
day,
|
||||||
|
month,
|
||||||
|
timestamp,
|
||||||
|
dayString: daysMap[day],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNumberOfDays = (year, month) => {
|
||||||
|
return 40 - new Date(year, month, 40).getDate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMonthDetails = (year, month) => {
|
||||||
|
let firstDay = new Date(year, month).getDay();
|
||||||
|
let numberOfDays = getNumberOfDays(year, month);
|
||||||
|
let monthArray = [];
|
||||||
|
let rows = 6;
|
||||||
|
let currentDay = null;
|
||||||
|
let index = 0;
|
||||||
|
let cols = 7;
|
||||||
|
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
for (let col = 0; col < cols; col++) {
|
||||||
|
currentDay = getDayDetails({
|
||||||
|
index,
|
||||||
|
numberOfDays,
|
||||||
|
firstDay,
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
});
|
||||||
|
monthArray.push(currentDay);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return monthArray;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCurrentDay = (day) => {
|
||||||
|
return day.timestamp === todayTimestamp;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSelectedDay = (day) => {
|
||||||
|
return day.timestamp === state.selectedDay;
|
||||||
|
};
|
||||||
|
const isSelectedRange = (day) => {
|
||||||
|
if (!state.selectedRange.after || !state.selectedRange.before) return;
|
||||||
|
|
||||||
|
return day.timestamp < state.selectedRange.before * 1000 && day.timestamp >= state.selectedRange.after * 1000;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDateFromDateString = (dateValue) => {
|
||||||
|
const dateData = dateValue.split('-').map((d) => parseInt(d, 10));
|
||||||
|
if (dateData.length < 3) return null;
|
||||||
|
|
||||||
|
const year = dateData[0];
|
||||||
|
const month = dateData[1];
|
||||||
|
const date = dateData[2];
|
||||||
|
return { year, month, date };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMonthStr = (month) => monthMap[Math.max(Math.min(11, month), 0)] || 'Month';
|
||||||
|
|
||||||
|
const getDateStringFromTimestamp = (timestamp) => {
|
||||||
|
let dateObject = new Date(timestamp);
|
||||||
|
let month = dateObject.getMonth() + 1;
|
||||||
|
let date = dateObject.getDate();
|
||||||
|
return dateObject.getFullYear() + '-' + (month < 10 ? '0' + month : month) + '-' + (date < 10 ? '0' + date : date);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setDate = (dateData) => {
|
||||||
|
let selectedDay = new Date(dateData.year, dateData.month - 1, dateData.date).getTime();
|
||||||
|
setState((prev) => ({ ...prev, selectedDay }));
|
||||||
|
|
||||||
|
if (onChange) {
|
||||||
|
// onChange(selectedDay);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDateFromInput = () => {
|
||||||
|
let dateValue = inputRef.current.value;
|
||||||
|
let dateData = getDateFromDateString(dateValue);
|
||||||
|
if (dateData !== null) {
|
||||||
|
setDate(dateData);
|
||||||
|
setState(
|
||||||
|
(prev = {
|
||||||
|
...prev,
|
||||||
|
year: dateData.year,
|
||||||
|
month: dateData.month - 1,
|
||||||
|
monthDetails: getMonthDetails(dateData.year, dateData.month - 1),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// const setDateToInput = (timestamp) => {
|
||||||
|
// const dateString = getDateStringFromTimestamp(timestamp);
|
||||||
|
// inputRef.current.value = dateString;
|
||||||
|
// };
|
||||||
|
|
||||||
|
const onDateClick = (day) => {
|
||||||
|
const range = {
|
||||||
|
selectedRange: state.selectedRangeBefore
|
||||||
|
? { ...state.selectedRange, before: new Date(day.timestamp).setHours(24, 0, 0, 0) / 1000 }
|
||||||
|
: { ...state.selectedRange, after: day.timestamp / 1000 },
|
||||||
|
};
|
||||||
|
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
...range,
|
||||||
|
selectedDay: day.timestamp,
|
||||||
|
selectedRangeBefore: !state.selectedRangeBefore,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// setDateToInput(day.timestamp);
|
||||||
|
if (onChange) {
|
||||||
|
onChange([{ before: range.selectedRange.before, after: range.selectedRange.after }]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setYear = (offset) => {
|
||||||
|
const year = state.year + offset;
|
||||||
|
const month = state.month;
|
||||||
|
setState((prev) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
year,
|
||||||
|
monthDetails: getMonthDetails(year, month),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setMonth = (offset) => {
|
||||||
|
const year = state.year;
|
||||||
|
const month = state.month + offset;
|
||||||
|
if (month === -1) {
|
||||||
|
month = 11;
|
||||||
|
year--;
|
||||||
|
} else if (month === 12) {
|
||||||
|
month = 0;
|
||||||
|
year++;
|
||||||
|
}
|
||||||
|
setState((prev) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
monthDetails: getMonthDetails(year, month),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderers
|
||||||
|
*/
|
||||||
|
|
||||||
|
const renderCalendar = () => {
|
||||||
|
let days =
|
||||||
|
state.monthDetails &&
|
||||||
|
state.monthDetails.map((day, index) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => onDateClick(day)}
|
||||||
|
className={
|
||||||
|
'h-12 w-12 float-left flex flex-shrink justify-center items-center cursor-pointer ' +
|
||||||
|
(day.month !== 0 ? ' opacity-50 bg-gray-700 dark:bg-gray-700 pointer-events-none' : '') +
|
||||||
|
(isCurrentDay(day) ? 'rounded-full bg-gray-100 dark:hover:bg-gray-100 ' : '') +
|
||||||
|
(isSelectedDay(day) ? 'bg-gray-100 dark:hover:bg-gray-100' : '') +
|
||||||
|
(isSelectedRange(day) ? ' bg-blue-500 dark:hover:bg-blue-500' : '')
|
||||||
|
}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<div className=" font-light ">
|
||||||
|
<span className="text-gray-400">{day.date}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="w-full flex justify-start flex-shrink">
|
||||||
|
{['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'].map((d, i) => (
|
||||||
|
<div key={i} className="w-12 text-xs font-light text-center">
|
||||||
|
{d}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-56">{days}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="select-none w-96 flex flex-shrink" ref={calenderRef}>
|
||||||
|
<div className="py-4 px-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-1/6 relative flex justify-around">
|
||||||
|
<div
|
||||||
|
className="flex justify-center items-center cursor-pointer absolute -mt-4 text-center rounded-full w-10 h-10 bg-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800"
|
||||||
|
onClick={() => setYear(-1)}
|
||||||
|
>
|
||||||
|
<ArrowRightDouble className="h-2/6 transform rotate-180 " />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/6 relative flex justify-around ">
|
||||||
|
<div
|
||||||
|
className="flex justify-center items-center cursor-pointer absolute -mt-4 text-center rounded-full w-10 h-10 bg-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800"
|
||||||
|
onClick={() => setMonth(-1)}
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-2/6 transform rotate-180 red" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/3">
|
||||||
|
<div className="text-3xl text-center text-gray-200 font-extralight">{state.year}</div>
|
||||||
|
<div className="text-center text-gray-400 font-extralight">{getMonthStr(state.month)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/6 relative flex justify-around ">
|
||||||
|
<div
|
||||||
|
className="flex justify-center items-center cursor-pointer absolute -mt-4 text-center rounded-full w-10 h-10 bg-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800"
|
||||||
|
onClick={() => setMonth(1)}
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-2/6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/6 relative flex justify-around " onClick={() => setYear(1)}>
|
||||||
|
<div className="flex justify-center items-center cursor-pointer absolute -mt-4 text-center rounded-full w-10 h-10 bg-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800">
|
||||||
|
<ArrowRightDouble className="h-2/6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3">{renderCalendar()}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Calender;
|
||||||
192
web/src/components/DatePicker.jsx
Normal file
192
web/src/components/DatePicker.jsx
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
export const DateFilterOptions = [
|
||||||
|
{
|
||||||
|
label: 'All',
|
||||||
|
value: ['all'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Today',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
//Before
|
||||||
|
before: new Date().setHours(24, 0, 0, 0) / 1000,
|
||||||
|
//After
|
||||||
|
after: new Date().setHours(0, 0, 0, 0) / 1000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Yesterday',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
//Before
|
||||||
|
before: new Date(new Date().setDate(new Date().getDate() - 1)).setHours(24, 0, 0, 0) / 1000,
|
||||||
|
//After
|
||||||
|
after: new Date(new Date().setDate(new Date().getDate() - 1)).setHours(0, 0, 0, 0) / 1000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Last 7 Days',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
//Before
|
||||||
|
before: new Date().setHours(24, 0, 0, 0) / 1000,
|
||||||
|
//After
|
||||||
|
after: new Date(new Date().setDate(new Date().getDate() - 7)).setHours(0, 0, 0, 0) / 1000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'This Month',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
//Before
|
||||||
|
before: new Date().setHours(24, 0, 0, 0) / 1000,
|
||||||
|
//After
|
||||||
|
after: new Date(new Date().getFullYear(), new Date().getMonth(), 1).getTime() / 1000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Last Month',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
//Before
|
||||||
|
before: new Date(new Date().getFullYear(), new Date().getMonth(), 1).getTime() / 1000,
|
||||||
|
//After
|
||||||
|
after: new Date(new Date().getFullYear(), new Date().getMonth() - 1, 1).getTime() / 1000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Custom Range',
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function DatePicker({
|
||||||
|
helpText,
|
||||||
|
keyboardType = 'date',
|
||||||
|
inputRef,
|
||||||
|
label,
|
||||||
|
leadingIcon: LeadingIcon,
|
||||||
|
onBlur,
|
||||||
|
onChangeText,
|
||||||
|
onFocus,
|
||||||
|
readonly,
|
||||||
|
trailingIcon: TrailingIcon,
|
||||||
|
value: propValue = '',
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
const [isFocused, setFocused] = useState(false);
|
||||||
|
const [value, setValue] = useState(propValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (propValue !== value) {
|
||||||
|
setValue(propValue);
|
||||||
|
}
|
||||||
|
// DO NOT include `value`
|
||||||
|
}, [propValue, setValue]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDateToInput(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFocus = useCallback(
|
||||||
|
(event) => {
|
||||||
|
setFocused(true);
|
||||||
|
onFocus && onFocus(event);
|
||||||
|
},
|
||||||
|
[onFocus]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleBlur = useCallback(
|
||||||
|
(event) => {
|
||||||
|
setFocused(false);
|
||||||
|
onBlur && onBlur(event);
|
||||||
|
},
|
||||||
|
[onBlur]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(event) => {
|
||||||
|
const { value } = event.target;
|
||||||
|
setValue(value);
|
||||||
|
onChangeText && onChangeText(value);
|
||||||
|
},
|
||||||
|
[onChangeText, setValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getDateFromDateString = (dateValue) => {
|
||||||
|
const dateData = dateValue.split('-').map((d) => parseInt(d, 10));
|
||||||
|
if (dateData.length < 3) return null;
|
||||||
|
|
||||||
|
const year = dateData[0];
|
||||||
|
const month = dateData[1];
|
||||||
|
const date = dateData[2];
|
||||||
|
return { year, month, date };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDateStringFromTimestamp = (timestamp) => {
|
||||||
|
const dateObject = new Date(timestamp * 1000);
|
||||||
|
const month = dateObject.getMonth() + 1;
|
||||||
|
const date = dateObject.getDate();
|
||||||
|
return dateObject.getFullYear() + '-' + (month < 10 ? '0' + month : month) + '-' + (date < 10 ? '0' + date : date);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setDateClick = (dateData) => {
|
||||||
|
const selectedDay = new Date(dateData.year, dateData.month - 1, dateData.date).getTime();
|
||||||
|
if (props.onchange) {
|
||||||
|
props.onchange(new Date(selectedDay).getTime() / 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setDateToInput = (timestamp) => {
|
||||||
|
const dateString = getDateStringFromTimestamp(timestamp);
|
||||||
|
// inputRef.current.value = dateString;
|
||||||
|
};
|
||||||
|
const onClick = (e) => {
|
||||||
|
props.onclick(e);
|
||||||
|
};
|
||||||
|
const labelMoved = isFocused || value !== '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
{props.children}
|
||||||
|
<div
|
||||||
|
className={`bg-gray-100 dark:bg-gray-700 rounded rounded-b-none border-gray-400 border-b p-1 pl-4 pr-3 ${
|
||||||
|
isFocused ? 'border-blue-500 dark:border-blue-500' : ''
|
||||||
|
}`}
|
||||||
|
ref={inputRef}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="flex space-x-2 items-center"
|
||||||
|
data-testid={`label-${label.toLowerCase().replace(/[^\w]+/g, '_')}`}
|
||||||
|
>
|
||||||
|
<div className="relative w-full">
|
||||||
|
<input
|
||||||
|
className="h-6 mt-6 w-full bg-transparent focus:outline-none focus:ring-0"
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onInput={handleChange}
|
||||||
|
tabIndex="0"
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`absolute top-3 transition transform text-gray-600 dark:text-gray-400 ${
|
||||||
|
labelMoved ? 'text-xs -translate-y-2' : ''
|
||||||
|
} ${isFocused ? 'text-blue-500 dark:text-blue-500' : ''}`}
|
||||||
|
>
|
||||||
|
<p>{label}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -79,7 +79,7 @@ export default function RelativeModal({
|
|||||||
}
|
}
|
||||||
// too close to bottom
|
// too close to bottom
|
||||||
if (top + menuHeight > windowHeight - WINDOW_PADDING + window.scrollY) {
|
if (top + menuHeight > windowHeight - WINDOW_PADDING + window.scrollY) {
|
||||||
newTop = relativeToY - menuHeight;
|
newTop = WINDOW_PADDING;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (top <= WINDOW_PADDING + window.scrollY) {
|
if (top <= WINDOW_PADDING + window.scrollY) {
|
||||||
|
|||||||
@ -3,29 +3,56 @@ import ArrowDropdown from '../icons/ArrowDropdown';
|
|||||||
import ArrowDropup from '../icons/ArrowDropup';
|
import ArrowDropup from '../icons/ArrowDropup';
|
||||||
import Menu, { MenuItem } from './Menu';
|
import Menu, { MenuItem } from './Menu';
|
||||||
import TextField from './TextField';
|
import TextField from './TextField';
|
||||||
|
import DatePicker from './DatePicker';
|
||||||
|
import Calender from './Calender';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
|
|
||||||
export default function Select({ label, onChange, options: inputOptions = [], selected: propSelected }) {
|
export default function Select({
|
||||||
|
type,
|
||||||
|
label,
|
||||||
|
onChange,
|
||||||
|
paramName,
|
||||||
|
options: inputOptions = [],
|
||||||
|
selected: propSelected,
|
||||||
|
}) {
|
||||||
const options = useMemo(
|
const options = useMemo(
|
||||||
() =>
|
() =>
|
||||||
typeof inputOptions[0] === 'string' ? inputOptions.map((opt) => ({ value: opt, label: opt })) : inputOptions,
|
typeof inputOptions[1] === 'string' ? inputOptions.map((opt) => ({ value: opt, label: opt })) : inputOptions,
|
||||||
[inputOptions]
|
[inputOptions]
|
||||||
);
|
);
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
|
||||||
const [selected, setSelected] = useState(
|
|
||||||
Math.max(
|
|
||||||
options.findIndex(({ value }) => value === propSelected),
|
|
||||||
0
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const [focused, setFocused] = useState(null);
|
|
||||||
|
|
||||||
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
|
const [selected, setSelected] = useState();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelected(
|
||||||
|
Math.max(
|
||||||
|
options.findIndex(({ value }) => propSelected.includes(value)),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [options, propSelected]);
|
||||||
|
|
||||||
|
const [focused, setFocused] = useState(null);
|
||||||
|
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||||
|
const calenderRef = useRef(null);
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(value, label) => {
|
(value, label) => {
|
||||||
setSelected(options.findIndex((opt) => opt.value === value));
|
setSelected(options.findIndex((opt) => opt.value === value));
|
||||||
|
setShowMenu(false);
|
||||||
|
|
||||||
|
if (!value) return setShowDatePicker(true);
|
||||||
onChange && onChange(value, label);
|
onChange && onChange(value, label);
|
||||||
|
},
|
||||||
|
[onChange, options]
|
||||||
|
);
|
||||||
|
const handleDateRange = useCallback(
|
||||||
|
(range) => {
|
||||||
|
// setSelected(options.findIndex((opt) => opt.value === value));
|
||||||
|
|
||||||
|
onChange && onChange(range, 'range');
|
||||||
setShowMenu(false);
|
setShowMenu(false);
|
||||||
},
|
},
|
||||||
[onChange, options]
|
[onChange, options]
|
||||||
@ -85,25 +112,80 @@ export default function Select({ label, onChange, options: inputOptions = [], se
|
|||||||
// DO NOT include `selected`
|
// DO NOT include `selected`
|
||||||
}, [options, propSelected]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [options, propSelected]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<Fragment>
|
window.addEventListener('click', addBackDrop);
|
||||||
<TextField
|
// setDateToInput(state.selectedDay);
|
||||||
inputRef={ref}
|
return function cleanup() {
|
||||||
label={label}
|
window.removeEventListener('click', addBackDrop);
|
||||||
onchange={onChange}
|
};
|
||||||
onclick={handleClick}
|
}, [showDatePicker]);
|
||||||
onkeydown={handleKeydown}
|
|
||||||
readonly
|
const findDOMNode = (component) => {
|
||||||
trailingIcon={showMenu ? ArrowDropup : ArrowDropdown}
|
return (component && (component.base || (component.nodeType === 1 && component))) || null;
|
||||||
value={options[selected]?.label}
|
};
|
||||||
/>
|
|
||||||
{showMenu ? (
|
const addBackDrop = (e) => {
|
||||||
<Menu className="rounded-t-none" onDismiss={handleDismiss} relativeTo={ref} widthRelative>
|
if (showDatePicker && !findDOMNode(calenderRef.current).contains(e.target)) {
|
||||||
{options.map(({ value, label }, i) => (
|
setShowDatePicker(false);
|
||||||
<MenuItem key={value} label={label} focus={focused === i} onSelect={handleSelect} value={value} />
|
}
|
||||||
))}
|
};
|
||||||
</Menu>
|
|
||||||
) : null}
|
switch (type) {
|
||||||
</Fragment>
|
case 'datepicker':
|
||||||
);
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<DatePicker
|
||||||
|
inputRef={ref}
|
||||||
|
label={label}
|
||||||
|
onchange={onChange}
|
||||||
|
onclick={handleClick}
|
||||||
|
onkeydown={handleKeydown}
|
||||||
|
trailingIcon={showMenu ? ArrowDropup : ArrowDropdown}
|
||||||
|
value={options[selected]}
|
||||||
|
></DatePicker>
|
||||||
|
{showDatePicker && (
|
||||||
|
<Menu className="rounded-t-none" onDismiss={handleDismiss} relativeTo={ref}>
|
||||||
|
<Calender onChange={handleDateRange} calenderRef={calenderRef} />
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
{showMenu ? (
|
||||||
|
<Menu className="rounded-t-none" onDismiss={handleDismiss} relativeTo={ref} widthRelative>
|
||||||
|
{options.map(({ value, label }, i) => (
|
||||||
|
<MenuItem key={value} label={label} focus={focused === i} onSelect={handleSelect} value={value} />
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
) : null}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
case 'dropdown':
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<TextField
|
||||||
|
inputRef={ref}
|
||||||
|
label={label}
|
||||||
|
onchange={onChange}
|
||||||
|
onclick={handleClick}
|
||||||
|
onkeydown={handleKeydown}
|
||||||
|
readonly
|
||||||
|
trailingIcon={showMenu ? ArrowDropup : ArrowDropdown}
|
||||||
|
value={options[selected]?.label}
|
||||||
|
/>
|
||||||
|
{showMenu ? (
|
||||||
|
<Menu className="rounded-t-none" onDismiss={handleDismiss} relativeTo={ref} widthRelative>
|
||||||
|
{options.map(({ value, label }, i) => (
|
||||||
|
<MenuItem
|
||||||
|
key={value}
|
||||||
|
label={label}
|
||||||
|
focus={focused === i}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
value={[{ [paramName]: value }]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
) : null}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
web/src/icons/ArrowLeft.jsx
Normal file
18
web/src/icons/ArrowLeft.jsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { memo } from 'preact/compat';
|
||||||
|
|
||||||
|
export function ArrowLeft({ className = '' }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={`fill-current ${className}`}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path d="M12 0c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm-1.218 19l-1.782-1.75 5.25-5.25-5.25-5.25 1.782-1.75 6.968 7-6.968 7z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(ArrowLeft);
|
||||||
12
web/src/icons/ArrowRight.jsx
Normal file
12
web/src/icons/ArrowRight.jsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { memo } from 'preact/compat';
|
||||||
|
|
||||||
|
export function ArrowRight({ className = '' }) {
|
||||||
|
return (
|
||||||
|
<svg className={`fill-current ${className}`} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path d="M5 3l3.057-3 11.943 12-11.943 12-3.057-3 9-9z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(ArrowRight);
|
||||||
12
web/src/icons/ArrowRightDouble.jsx
Normal file
12
web/src/icons/ArrowRightDouble.jsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { memo } from 'preact/compat';
|
||||||
|
|
||||||
|
export function ArrowRightDouble({ className = '' }) {
|
||||||
|
return (
|
||||||
|
<svg className={`fill-current ${className}`} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path d="M0 3.795l2.995-2.98 11.132 11.185-11.132 11.186-2.995-2.981 8.167-8.205-8.167-8.205zm18.04 8.205l-8.167 8.205 2.995 2.98 11.132-11.185-11.132-11.186-2.995 2.98 8.167 8.206z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(ArrowRightDouble);
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { h } from 'preact';
|
import { h, Fragment } from 'preact';
|
||||||
import ActivityIndicator from '../components/ActivityIndicator';
|
import ActivityIndicator from '../components/ActivityIndicator';
|
||||||
import Heading from '../components/Heading';
|
import Heading from '../components/Heading';
|
||||||
import Link from '../components/Link';
|
import Link from '../components/Link';
|
||||||
@ -9,6 +9,7 @@ import { useIntersectionObserver } from '../hooks';
|
|||||||
import { FetchStatus, useApiHost, useConfig, useEvents } from '../api';
|
import { FetchStatus, useApiHost, useConfig, useEvents } from '../api';
|
||||||
import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from '../components/Table';
|
import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from '../components/Table';
|
||||||
import { useCallback, useEffect, useMemo, useReducer, useState } from 'preact/hooks';
|
import { useCallback, useEffect, useMemo, useReducer, useState } from 'preact/hooks';
|
||||||
|
import { DateFilterOptions } from '../components/DatePicker';
|
||||||
|
|
||||||
const API_LIMIT = 25;
|
const API_LIMIT = 25;
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ const defaultSearchString = (limit) => `include_thumbnails=0&limit=${limit}`;
|
|||||||
function removeDefaultSearchKeys(searchParams) {
|
function removeDefaultSearchKeys(searchParams) {
|
||||||
searchParams.delete('limit');
|
searchParams.delete('limit');
|
||||||
searchParams.delete('include_thumbnails');
|
searchParams.delete('include_thumbnails');
|
||||||
searchParams.delete('before');
|
// searchParams.delete('before');
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Events({ path: pathname, limit = API_LIMIT } = {}) {
|
export default function Events({ path: pathname, limit = API_LIMIT } = {}) {
|
||||||
@ -101,6 +102,7 @@ export default function Events({ path: pathname, limit = API_LIMIT } = {}) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
|
const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 w-full">
|
<div className="space-y-4 w-full">
|
||||||
<Heading>Events</Heading>
|
<Heading>Events</Heading>
|
||||||
@ -249,23 +251,34 @@ function Filters({ onChange, searchParams }) {
|
|||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex space-x-4">
|
<Fragment>
|
||||||
<Filter onChange={onChange} options={cameras} paramName="camera" searchParams={searchParams} />
|
<div className="flex space-x-4">
|
||||||
<Filter onChange={onChange} options={zones} paramName="zone" searchParams={searchParams} />
|
<Filter
|
||||||
<Filter onChange={onChange} options={labels} paramName="label" searchParams={searchParams} />
|
type="dropdown"
|
||||||
</div>
|
onChange={onChange}
|
||||||
|
options={cameras}
|
||||||
|
paramName={['camera']}
|
||||||
|
label="Camera"
|
||||||
|
searchParams={searchParams}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Filter({ onChange, searchParams, paramName, options }) {
|
function Filter({ onChange, searchParams, paramName, options, type, ...rest }) {
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(key) => {
|
(key) => {
|
||||||
const newParams = new URLSearchParams(searchParams.toString());
|
const newParams = new URLSearchParams(searchParams.toString());
|
||||||
if (key !== 'all') {
|
key.map((queryArray) => {
|
||||||
newParams.set(paramName, key);
|
if (queryArray[paramName] !== 'all') {
|
||||||
} else {
|
for (let query in queryArray) {
|
||||||
newParams.delete(paramName);
|
newParams.set(query, queryArray[query]);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
paramName.map((p) => newParams.delete(p));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onChange(newParams);
|
onChange(newParams);
|
||||||
},
|
},
|
||||||
@ -273,13 +286,16 @@ function Filter({ onChange, searchParams, paramName, options }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const selectOptions = useMemo(() => ['all', ...options], [options]);
|
const selectOptions = useMemo(() => ['all', ...options], [options]);
|
||||||
|
const selected = useMemo(() => paramName.map((p) => searchParams.get(p) || 'all'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
label={`${paramName.charAt(0).toUpperCase()}${paramName.substr(1)}`}
|
|
||||||
onChange={handleSelect}
|
onChange={handleSelect}
|
||||||
options={selectOptions}
|
options={selectOptions}
|
||||||
selected={searchParams.get(paramName) || 'all'}
|
selected={selected}
|
||||||
|
paramName={paramName}
|
||||||
|
type={type}
|
||||||
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user