fix date picker

This commit is contained in:
Blake Blackshear 2022-03-11 07:32:36 -06:00
parent 1d8f1b24a9
commit deb3536cb2
3 changed files with 162 additions and 142 deletions

View File

@ -15,7 +15,8 @@
"rules": { "rules": {
"indent": ["error", 2, { "SwitchCase": 1 }], "indent": ["error", 2, { "SwitchCase": 1 }],
"comma-dangle": ["error", { "objects": "always-multiline", "arrays": "always-multiline" }], "comma-dangle": ["error", { "objects": "always-multiline", "arrays": "always-multiline" }],
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"no-console": "error"
}, },
"overrides": [ "overrides": [
{ {

View File

@ -5,7 +5,7 @@ import ArrowRightDouble from '../icons/ArrowRightDouble';
const todayTimestamp = new Date().setHours(0, 0, 0, 0).valueOf(); const todayTimestamp = new Date().setHours(0, 0, 0, 0).valueOf();
const Calendar = ({ onChange, calendarRef, close }) => { const Calendar = ({ onChange, calendarRef, close, dateRange }) => {
const keyRef = useRef([]); const keyRef = useRef([]);
const date = new Date(); const date = new Date();
@ -36,7 +36,7 @@ const Calendar = ({ onChange, calendarRef, close }) => {
year, year,
month, month,
selectedDay: null, selectedDay: null,
timeRange: { before: null, after: null }, timeRange: dateRange,
monthDetails: null, monthDetails: null,
}); });

View File

@ -42,53 +42,55 @@ export default function Events({ path, ...props }) {
label: props.label ?? 'all', label: props.label ?? 'all',
zone: props.zone ?? 'all', zone: props.zone ?? 'all',
}); });
const [state, setState] = useState({
showDownloadMenu: null,
showDatePicker: null,
showCalendar: null,
});
const [viewEvent, setViewEvent] = useState(); const [viewEvent, setViewEvent] = useState();
const [downloadEvent, setDownloadEvent] = useState({ id: null, has_clip: false, has_snapshot: false }); const [downloadEvent, setDownloadEvent] = useState({ id: null, has_clip: false, has_snapshot: false });
const [showDownloadMenu, setShowDownloadMenu] = useState();
const [showDatePicker, setShowDatePicker] = useState();
const [showCalendar, setShowCalendar] = useState();
const eventsFetcher = (path, params) => { const eventsFetcher = useCallback((path, params) => {
params = { ...params, include_thumbnails: 0, limit: API_LIMIT }; params = { ...params, include_thumbnails: 0, limit: API_LIMIT };
return axios.get(path, { params }).then((res) => res.data); return axios.get(path, { params }).then((res) => res.data);
}; }, []);
const getKey = (index, prevData) => { const getKey = useCallback(
if (index > 0) { (index, prevData) => {
const lastDate = prevData[prevData.length - 1].start_time; if (index > 0) {
const pagedParams = { ...searchParams, before: lastDate }; const lastDate = prevData[prevData.length - 1].start_time;
return ['events', pagedParams]; const pagedParams = { ...searchParams, before: lastDate };
} return ['events', pagedParams];
}
return ['events', searchParams]; return ['events', searchParams];
}; },
[searchParams]
);
const { data: eventPages, mutate, size, setSize, isValidating } = useSWRInfinite(getKey, eventsFetcher); const { data: eventPages, mutate, size, setSize, isValidating } = useSWRInfinite(getKey, eventsFetcher);
const { data: config } = useSWR('config'); const { data: config } = useSWR('config');
const cameras = useMemo(() => Object.keys(config?.cameras || {}), [config]); const filterValues = useMemo(
() => ({
const zones = useMemo( cameras: Object.keys(config?.cameras || {}),
() => zones: Object.values(config?.cameras || {})
Object.values(config?.cameras || {})
.reduce((memo, camera) => { .reduce((memo, camera) => {
memo = memo.concat(Object.keys(camera?.zones || {})); memo = memo.concat(Object.keys(camera?.zones || {}));
return memo; return memo;
}, []) }, [])
.filter((value, i, self) => self.indexOf(value) === i), .filter((value, i, self) => self.indexOf(value) === i),
labels: Object.values(config?.cameras || {})
.reduce((memo, camera) => {
memo = memo.concat(camera?.objects?.track || []);
return memo;
}, config?.objects?.track || [])
.filter((value, i, self) => self.indexOf(value) === i),
}),
[config] [config]
); );
const labels = useMemo(() => {
return Object.values(config?.cameras || {})
.reduce((memo, camera) => {
memo = memo.concat(camera?.objects?.track || []);
return memo;
}, config?.objects?.track || [])
.filter((value, i, self) => self.indexOf(value) === i);
}, [config]);
const onSave = async (e, eventId, save) => { const onSave = async (e, eventId, save) => {
e.stopPropagation(); e.stopPropagation();
let response; let response;
@ -118,16 +120,15 @@ export default function Events({ path, ...props }) {
e.stopPropagation(); e.stopPropagation();
setDownloadEvent((_prev) => ({ id: event.id, has_clip: event.has_clip, has_snapshot: event.has_snapshot })); setDownloadEvent((_prev) => ({ id: event.id, has_clip: event.has_clip, has_snapshot: event.has_snapshot }));
downloadButton.current = e.target; downloadButton.current = e.target;
setShowDownloadMenu(true); setState({ ...state, showDownloadMenu: true });
}; };
const handleSelectDateRange = useCallback( const handleSelectDateRange = useCallback(
(dates) => { (dates) => {
console.log(dates);
setSearchParams({ ...searchParams, before: dates.before, after: dates.after }); setSearchParams({ ...searchParams, before: dates.before, after: dates.after });
setShowDatePicker(false); setState({ ...state, showDatePicker: false });
}, },
[searchParams, setSearchParams, setShowDatePicker] [searchParams, setSearchParams, state, setState]
); );
const onFilter = useCallback( const onFilter = useCallback(
@ -166,7 +167,7 @@ export default function Events({ path, ...props }) {
[size, setSize, isValidating, isDone] [size, setSize, isValidating, isDone]
); );
if (!eventPages || !config) { if (!config) {
return <ActivityIndicator />; return <ActivityIndicator />;
} }
@ -180,7 +181,7 @@ export default function Events({ path, ...props }) {
onChange={(e) => onFilter('camera', e.target.value)} onChange={(e) => onFilter('camera', e.target.value)}
> >
<option value="all">all</option> <option value="all">all</option>
{cameras.map((item) => ( {filterValues.cameras.map((item) => (
<option key={item} value={item}> <option key={item} value={item}>
{item} {item}
</option> </option>
@ -192,7 +193,7 @@ export default function Events({ path, ...props }) {
onChange={(e) => onFilter('label', e.target.value)} onChange={(e) => onFilter('label', e.target.value)}
> >
<option value="all">all</option> <option value="all">all</option>
{labels.map((item) => ( {filterValues.labels.map((item) => (
<option key={item} value={item}> <option key={item} value={item}>
{item} {item}
</option> </option>
@ -204,18 +205,21 @@ export default function Events({ path, ...props }) {
onChange={(e) => onFilter('zone', e.target.value)} onChange={(e) => onFilter('zone', e.target.value)}
> >
<option value="all">all</option> <option value="all">all</option>
{zones.map((item) => ( {filterValues.zones.map((item) => (
<option key={item} value={item}> <option key={item} value={item}>
{item} {item}
</option> </option>
))} ))}
</select> </select>
<div ref={datePicker} className="ml-auto"> <div ref={datePicker} className="ml-auto">
<CalendarIcon className="h-8 w-8 cursor-pointer" onClick={() => setShowDatePicker(true)} /> <CalendarIcon
className="h-8 w-8 cursor-pointer"
onClick={() => setState({ ...state, showDatePicker: true })}
/>
</div> </div>
</div> </div>
{showDownloadMenu && ( {state.showDownloadMenu && (
<Menu onDismiss={() => setShowDownloadMenu(false)} relativeTo={downloadButton}> <Menu onDismiss={() => setState({ ...state, showDownloadMenu: false })} relativeTo={downloadButton}>
{downloadEvent.has_snapshot && ( {downloadEvent.has_snapshot && (
<MenuItem <MenuItem
icon={Snapshot} icon={Snapshot}
@ -236,8 +240,12 @@ export default function Events({ path, ...props }) {
)} )}
</Menu> </Menu>
)} )}
{showDatePicker && ( {state.showDatePicker && (
<Menu className="rounded-t-none" onDismiss={() => setShowDatePicker(false)} relativeTo={datePicker}> <Menu
className="rounded-t-none"
onDismiss={() => setState({ ...state, setShowDatePicker: false })}
relativeTo={datePicker}
>
<MenuItem label="All" value={{ before: null, after: null }} onSelect={handleSelectDateRange} /> <MenuItem label="All" value={{ before: null, after: null }} onSelect={handleSelectDateRange} />
<MenuItem label="Today" value={{ before: null, after: daysAgo(0) }} onSelect={handleSelectDateRange} /> <MenuItem label="Today" value={{ before: null, after: daysAgo(0) }} onSelect={handleSelectDateRange} />
<MenuItem <MenuItem
@ -256,119 +264,130 @@ export default function Events({ path, ...props }) {
label="Custom Range" label="Custom Range"
value="custom" value="custom"
onSelect={() => { onSelect={() => {
setShowCalendar(true); setState({ ...state, showCalendar: true, showDatePicker: false });
setShowDatePicker(false);
}} }}
/> />
</Menu> </Menu>
)} )}
{showCalendar && ( {state.showCalendar && (
<Menu className="rounded-t-none" onDismiss={() => setShowCalendar(false)} relativeTo={datePicker}> <Menu
<Calendar onChange={handleSelectDateRange} close={() => setShowCalendar(false)} /> className="rounded-t-none"
onDismiss={() => setState({ ...state, showCalendar: false })}
relativeTo={datePicker}
>
<Calendar
onChange={handleSelectDateRange}
dateRange={{ before: searchParams.before * 1000 || null, after: searchParams.after * 1000 || null }}
close={() => setState({ ...state, showCalendar: false })}
/>
</Menu> </Menu>
)} )}
<div className="space-y-2"> <div className="space-y-2">
{eventPages.map((page, i) => { {eventPages ? (
const lastPage = eventPages.length === i + 1; eventPages.map((page, i) => {
return page.map((event, j) => { const lastPage = eventPages.length === i + 1;
const lastEvent = lastPage && page.length === j + 1; return page.map((event, j) => {
return ( const lastEvent = lastPage && page.length === j + 1;
<Fragment key={event.id}> return (
<div <Fragment key={event.id}>
ref={lastEvent ? lastEventRef : false}
className="flex bg-slate-100 dark:bg-slate-800 rounded cursor-pointer min-w-[330px]"
onClick={() => (viewEvent === event.id ? setViewEvent(null) : setViewEvent(event.id))}
>
<div <div
className="relative rounded-l flex-initial min-w-[125px] h-[125px] bg-contain" ref={lastEvent ? lastEventRef : false}
style={{ className="flex bg-slate-100 dark:bg-slate-800 rounded cursor-pointer min-w-[330px]"
'background-image': `url(${apiHost}/api/events/${event.id}/thumbnail.jpg)`, onClick={() => (viewEvent === event.id ? setViewEvent(null) : setViewEvent(event.id))}
}}
> >
<StarRecording <div
className="h-6 w-6 text-yellow-300 absolute top-1 right-1 cursor-pointer" className="relative rounded-l flex-initial min-w-[125px] h-[125px] bg-contain"
onClick={(e) => onSave(e, event.id, !event.retain_indefinitely)} style={{
fill={event.retain_indefinitely ? 'currentColor' : 'none'} 'background-image': `url(${apiHost}/api/events/${event.id}/thumbnail.jpg)`,
/> }}
{event.end_time ? null : ( >
<div className="bg-slate-300 dark:bg-slate-700 absolute bottom-0 text-center w-full uppercase text-sm rounded-bl"> <StarRecording
In progress className="h-6 w-6 text-yellow-300 absolute top-1 right-1 cursor-pointer"
</div> onClick={(e) => onSave(e, event.id, !event.retain_indefinitely)}
)} fill={event.retain_indefinitely ? 'currentColor' : 'none'}
</div>
<div className="m-2 flex grow">
<div className="flex flex-col grow">
<div className="capitalize text-lg font-bold">
{event.label} ({(event.top_score * 100).toFixed(0)}%)
</div>
<div className="text-sm">
{new Date(event.start_time * 1000).toLocaleDateString()}{' '}
{new Date(event.start_time * 1000).toLocaleTimeString()}
</div>
<div className="capitalize text-sm flex align-center mt-1">
<Camera className="h-5 w-5 mr-2 inline" />
{event.camera}
</div>
<div className="capitalize text-sm flex align-center">
<Zone className="w-5 h-5 mr-2 inline" />
{event.zones.join(',')}
</div>
</div>
<div class="flex flex-col">
<Delete className="cursor-pointer" stroke="#f87171" onClick={(e) => onDelete(e, event.id)} />
<Download
className="h-6 w-6 mt-auto"
stroke={event.has_clip || event.has_snapshot ? '#3b82f6' : '#cbd5e1'}
onClick={(e) => onDownloadClick(e, event)}
/> />
</div> {event.end_time ? null : (
</div> <div className="bg-slate-300 dark:bg-slate-700 absolute bottom-0 text-center w-full uppercase text-sm rounded-bl">
</div> In progress
{viewEvent !== event.id ? null : (
<div className="space-y-4">
<div className="mx-auto">
{event.has_clip ? (
<>
<Heading size="lg">Clip</Heading>
<VideoPlayer
options={{
preload: 'auto',
autoplay: true,
sources: [
{
src: `${apiHost}/vod/event/${event.id}/index.m3u8`,
type: 'application/vnd.apple.mpegurl',
},
],
}}
seekOptions={{ forward: 10, back: 5 }}
onReady={() => {}}
/>
</>
) : (
<div className="flex justify-center">
<div>
<Heading size="sm">{event.has_snapshot ? 'Best Image' : 'Thumbnail'}</Heading>
<img
className="flex-grow-0"
src={
event.has_snapshot
? `${apiHost}/api/events/${event.id}/snapshot.jpg`
: `data:image/jpeg;base64,${event.thumbnail}`
}
alt={`${event.label} at ${(event.top_score * 100).toFixed(0)}% confidence`}
/>
</div>
</div> </div>
)} )}
</div> </div>
<div className="m-2 flex grow">
<div className="flex flex-col grow">
<div className="capitalize text-lg font-bold">
{event.label} ({(event.top_score * 100).toFixed(0)}%)
</div>
<div className="text-sm">
{new Date(event.start_time * 1000).toLocaleDateString()}{' '}
{new Date(event.start_time * 1000).toLocaleTimeString()}
</div>
<div className="capitalize text-sm flex align-center mt-1">
<Camera className="h-5 w-5 mr-2 inline" />
{event.camera}
</div>
<div className="capitalize text-sm flex align-center">
<Zone className="w-5 h-5 mr-2 inline" />
{event.zones.join(',')}
</div>
</div>
<div class="flex flex-col">
<Delete className="cursor-pointer" stroke="#f87171" onClick={(e) => onDelete(e, event.id)} />
<Download
className="h-6 w-6 mt-auto"
stroke={event.has_clip || event.has_snapshot ? '#3b82f6' : '#cbd5e1'}
onClick={(e) => onDownloadClick(e, event)}
/>
</div>
</div>
</div> </div>
)} {viewEvent !== event.id ? null : (
</Fragment> <div className="space-y-4">
); <div className="mx-auto">
}); {event.has_clip ? (
})} <>
<Heading size="lg">Clip</Heading>
<VideoPlayer
options={{
preload: 'auto',
autoplay: true,
sources: [
{
src: `${apiHost}/vod/event/${event.id}/index.m3u8`,
type: 'application/vnd.apple.mpegurl',
},
],
}}
seekOptions={{ forward: 10, back: 5 }}
onReady={() => {}}
/>
</>
) : (
<div className="flex justify-center">
<div>
<Heading size="sm">{event.has_snapshot ? 'Best Image' : 'Thumbnail'}</Heading>
<img
className="flex-grow-0"
src={
event.has_snapshot
? `${apiHost}/api/events/${event.id}/snapshot.jpg`
: `data:image/jpeg;base64,${event.thumbnail}`
}
alt={`${event.label} at ${(event.top_score * 100).toFixed(0)}% confidence`}
/>
</div>
</div>
)}
</div>
</div>
)}
</Fragment>
);
});
})
) : (
<ActivityIndicator />
)}
</div> </div>
<div>{isDone ? null : <ActivityIndicator />}</div> <div>{isDone ? null : <ActivityIndicator />}</div>
</div> </div>