Add time range to side filter

This commit is contained in:
Nicolas Mowen 2024-10-15 16:09:51 -06:00
parent 9bce6efddc
commit af4a291cb2
2 changed files with 175 additions and 195 deletions

View File

@ -29,6 +29,7 @@ import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
import { FaArrowRight, FaClock } from "react-icons/fa"; import { FaArrowRight, FaClock } from "react-icons/fa";
import { useFormattedHour } from "@/hooks/use-date-utils"; import { useFormattedHour } from "@/hooks/use-date-utils";
import SearchFilterDialog from "../overlay/dialog/SearchFilterDialog"; import SearchFilterDialog from "../overlay/dialog/SearchFilterDialog";
import { CalendarRangeFilterButton } from "./CalendarFilterButton";
type SearchFilterGroupProps = { type SearchFilterGroupProps = {
className: string; className: string;
@ -166,7 +167,22 @@ export default function SearchFilterGroup({
}} }}
/> />
)} )}
{filters.includes("date") && (
<CalendarRangeFilterButton
range={
filter?.after == undefined || filter?.before == undefined
? undefined
: {
from: new Date(filter.after * 1000),
to: new Date(filter.before * 1000),
}
}
defaultText={isMobile ? "Dates" : "All Dates"}
updateSelectedRange={onUpdateSelectedRange}
/>
)}
<SearchFilterDialog <SearchFilterDialog
config={config}
filter={filter} filter={filter}
filterValues={filterValues} filterValues={filterValues}
groups={groups} groups={groups}
@ -349,184 +365,6 @@ export function GeneralFilterContent({
); );
} }
type TimeRangeFilterButtonProps = {
config?: FrigateConfig;
timeRange?: string;
updateTimeRange: (range: string | undefined) => void;
};
function TimeRangeFilterButton({
config,
timeRange,
updateTimeRange,
}: TimeRangeFilterButtonProps) {
const [open, setOpen] = useState(false);
const [startOpen, setStartOpen] = useState(false);
const [endOpen, setEndOpen] = useState(false);
const [afterHour, beforeHour] = useMemo(() => {
if (!timeRange || !timeRange.includes(",")) {
return [DEFAULT_TIME_RANGE_AFTER, DEFAULT_TIME_RANGE_BEFORE];
}
return timeRange.split(",");
}, [timeRange]);
const [selectedAfterHour, setSelectedAfterHour] = useState(afterHour);
const [selectedBeforeHour, setSelectedBeforeHour] = useState(beforeHour);
// format based on locale
const formattedAfter = useFormattedHour(config, afterHour);
const formattedBefore = useFormattedHour(config, beforeHour);
const formattedSelectedAfter = useFormattedHour(config, selectedAfterHour);
const formattedSelectedBefore = useFormattedHour(config, selectedBeforeHour);
useEffect(() => {
setSelectedAfterHour(afterHour);
setSelectedBeforeHour(beforeHour);
// only refresh when state changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [timeRange]);
const trigger = (
<Button
size="sm"
variant={timeRange ? "select" : "default"}
className="flex items-center gap-2 capitalize"
>
<FaClock
className={`${timeRange ? "text-selected-foreground" : "text-secondary-foreground"}`}
/>
<div
className={`${timeRange ? "text-selected-foreground" : "text-primary"}`}
>
{timeRange ? `${formattedAfter} - ${formattedBefore}` : "All Times"}
</div>
</Button>
);
const content = (
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
<div className="my-5 flex flex-row items-center justify-center gap-2">
<Popover
open={startOpen}
onOpenChange={(open) => {
if (!open) {
setStartOpen(false);
}
}}
>
<PopoverTrigger asChild>
<Button
className={`text-primary ${isDesktop ? "" : "text-xs"} `}
variant={startOpen ? "select" : "default"}
size="sm"
onClick={() => {
setStartOpen(true);
setEndOpen(false);
}}
>
{formattedSelectedAfter}
</Button>
</PopoverTrigger>
<PopoverContent className="flex flex-row items-center justify-center">
<input
className="text-md mx-4 w-full border border-input bg-background p-1 text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
id="startTime"
type="time"
value={selectedAfterHour}
step="60"
onChange={(e) => {
const clock = e.target.value;
const [hour, minute, _] = clock.split(":");
setSelectedAfterHour(`${hour}:${minute}`);
}}
/>
</PopoverContent>
</Popover>
<FaArrowRight className="size-4 text-primary" />
<Popover
open={endOpen}
onOpenChange={(open) => {
if (!open) {
setEndOpen(false);
}
}}
>
<PopoverTrigger asChild>
<Button
className={`text-primary ${isDesktop ? "" : "text-xs"}`}
variant={endOpen ? "select" : "default"}
size="sm"
onClick={() => {
setEndOpen(true);
setStartOpen(false);
}}
>
{formattedSelectedBefore}
</Button>
</PopoverTrigger>
<PopoverContent className="flex flex-col items-center">
<input
className="text-md mx-4 w-full border border-input bg-background p-1 text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
id="startTime"
type="time"
value={
selectedBeforeHour == "24:00" ? "23:59" : selectedBeforeHour
}
step="60"
onChange={(e) => {
const clock = e.target.value;
const [hour, minute, _] = clock.split(":");
setSelectedBeforeHour(`${hour}:${minute}`);
}}
/>
</PopoverContent>
</Popover>
</div>
<DropdownMenuSeparator />
<div className="flex items-center justify-evenly p-2">
<Button
variant="select"
onClick={() => {
if (
selectedAfterHour == DEFAULT_TIME_RANGE_AFTER &&
selectedBeforeHour == DEFAULT_TIME_RANGE_BEFORE
) {
updateTimeRange(undefined);
} else {
updateTimeRange(`${selectedAfterHour},${selectedBeforeHour}`);
}
setOpen(false);
}}
>
Apply
</Button>
<Button
onClick={() => {
setSelectedAfterHour(DEFAULT_TIME_RANGE_AFTER);
setSelectedBeforeHour(DEFAULT_TIME_RANGE_BEFORE);
updateTimeRange(undefined);
}}
>
Reset
</Button>
</div>
</div>
);
return (
<PlatformAwareDialog
trigger={trigger}
content={content}
open={open}
onOpenChange={(open) => {
setOpen(open);
}}
/>
);
}
type ZoneFilterButtonProps = { type ZoneFilterButtonProps = {
allZones: string[]; allZones: string[];
selectedZones?: string[]; selectedZones?: string[];

View File

@ -1,14 +1,27 @@
import { FaCog } from "react-icons/fa"; import { FaArrowRight, FaCog } from "react-icons/fa";
import { useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { PlatformAwareSheet } from "./PlatformAwareDialog"; import { PlatformAwareSheet } from "./PlatformAwareDialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { CamerasFilterContent } from "@/components/filter/CamerasFilterButton";
import useSWR from "swr"; import useSWR from "swr";
import { SearchFilter, SearchSource } from "@/types/search"; import {
import { CameraGroupConfig } from "@/types/frigateConfig"; DEFAULT_TIME_RANGE_AFTER,
DEFAULT_TIME_RANGE_BEFORE,
SearchFilter,
SearchSource,
} from "@/types/search";
import { CameraGroupConfig, FrigateConfig } from "@/types/frigateConfig";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { isDesktop } from "react-device-detect";
import { useFormattedHour } from "@/hooks/use-date-utils";
import Heading from "@/components/ui/heading";
type SearchFilterDialogProps = { type SearchFilterDialogProps = {
config?: FrigateConfig;
filter?: SearchFilter; filter?: SearchFilter;
filterValues: { filterValues: {
cameras: string[]; cameras: string[];
@ -20,6 +33,7 @@ type SearchFilterDialogProps = {
onUpdateFilter: (filter: SearchFilter) => void; onUpdateFilter: (filter: SearchFilter) => void;
}; };
export default function SearchFilterDialog({ export default function SearchFilterDialog({
config,
filter, filter,
filterValues, filterValues,
groups, groups,
@ -27,10 +41,8 @@ export default function SearchFilterDialog({
}: SearchFilterDialogProps) { }: SearchFilterDialogProps) {
// data // data
const [currentFilter, setCurrentFilter] = useState(filter ?? {});
const { data: allSubLabels } = useSWR(["sub_labels", { split_joined: 1 }]); const { data: allSubLabels } = useSWR(["sub_labels", { split_joined: 1 }]);
const [currentCameras, setCurrentCameras] = useState<string[] | undefined>(
filter?.cameras,
);
// state // state
@ -44,15 +56,12 @@ export default function SearchFilterDialog({
); );
const content = ( const content = (
<> <>
<CamerasFilterContent <TimeRangeFilterContent
allCameras={filterValues.cameras} config={config}
currentCameras={currentCameras} timeRange={currentFilter.time_range}
groups={groups} updateTimeRange={(newRange) =>
setCurrentCameras={setCurrentCameras} setCurrentFilter({ time_range: newRange, ...currentFilter })
setOpen={setOpen} }
updateCameraFilter={(newCameras) => {
onUpdateFilter({ ...filter, cameras: newCameras });
}}
/> />
</> </>
); );
@ -68,6 +77,139 @@ export default function SearchFilterDialog({
); );
} }
type TimeRangeFilterContentProps = {
config?: FrigateConfig;
timeRange?: string;
updateTimeRange: (range: string | undefined) => void;
};
function TimeRangeFilterContent({
config,
timeRange,
updateTimeRange,
}: TimeRangeFilterContentProps) {
const [startOpen, setStartOpen] = useState(false);
const [endOpen, setEndOpen] = useState(false);
const [afterHour, beforeHour] = useMemo(() => {
if (!timeRange || !timeRange.includes(",")) {
return [DEFAULT_TIME_RANGE_AFTER, DEFAULT_TIME_RANGE_BEFORE];
}
return timeRange.split(",");
}, [timeRange]);
const [selectedAfterHour, setSelectedAfterHour] = useState(afterHour);
const [selectedBeforeHour, setSelectedBeforeHour] = useState(beforeHour);
// format based on locale
const formattedSelectedAfter = useFormattedHour(config, selectedAfterHour);
const formattedSelectedBefore = useFormattedHour(config, selectedBeforeHour);
useEffect(() => {
setSelectedAfterHour(afterHour);
setSelectedBeforeHour(beforeHour);
// only refresh when state changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [timeRange]);
useEffect(() => {
if (
selectedAfterHour == DEFAULT_TIME_RANGE_AFTER &&
selectedBeforeHour == DEFAULT_TIME_RANGE_BEFORE
) {
updateTimeRange(undefined);
} else {
updateTimeRange(`${selectedAfterHour},${selectedBeforeHour}`);
}
// only refresh when state changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedAfterHour, selectedBeforeHour]);
return (
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
<Heading as="h4">Time Range</Heading>
<div className="my-5 flex flex-row items-center justify-center gap-2">
<Popover
open={startOpen}
onOpenChange={(open) => {
if (!open) {
setStartOpen(false);
}
}}
>
<PopoverTrigger asChild>
<Button
className={`text-primary ${isDesktop ? "" : "text-xs"} `}
variant={startOpen ? "select" : "default"}
size="sm"
onClick={() => {
setStartOpen(true);
setEndOpen(false);
}}
>
{formattedSelectedAfter}
</Button>
</PopoverTrigger>
<PopoverContent className="flex flex-row items-center justify-center">
<input
className="text-md mx-4 w-full border border-input bg-background p-1 text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
id="startTime"
type="time"
value={selectedAfterHour}
step="60"
onChange={(e) => {
const clock = e.target.value;
const [hour, minute, _] = clock.split(":");
setSelectedAfterHour(`${hour}:${minute}`);
}}
/>
</PopoverContent>
</Popover>
<FaArrowRight className="size-4 text-primary" />
<Popover
open={endOpen}
onOpenChange={(open) => {
if (!open) {
setEndOpen(false);
}
}}
>
<PopoverTrigger asChild>
<Button
className={`text-primary ${isDesktop ? "" : "text-xs"}`}
variant={endOpen ? "select" : "default"}
size="sm"
onClick={() => {
setEndOpen(true);
setStartOpen(false);
}}
>
{formattedSelectedBefore}
</Button>
</PopoverTrigger>
<PopoverContent className="flex flex-col items-center">
<input
className="text-md mx-4 w-full border border-input bg-background p-1 text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
id="startTime"
type="time"
value={
selectedBeforeHour == "24:00" ? "23:59" : selectedBeforeHour
}
step="60"
onChange={(e) => {
const clock = e.target.value;
const [hour, minute, _] = clock.split(":");
setSelectedBeforeHour(`${hour}:${minute}`);
}}
/>
</PopoverContent>
</Popover>
</div>
</div>
);
}
/** /**
* {filters.includes("date") && ( * {filters.includes("date") && (
<CalendarRangeFilterButton <CalendarRangeFilterButton