forked from Github/frigate
Add ability to filter search by time range (#13946)
* Add ability to filter by time range * Cleanup * Handle input with tags * fix input for time_range filter * fix before and after filters * clean up * Ensure the default value works as expected * Handle time range in am/pm based on browser * Fix arrow * Fix text * Handle midnight case * fix width * Fix bg * Fix bg * Fix mobile spacing * y spacing * remove left padding --------- Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||
import { DateRangePicker } from "../ui/calendar-range";
|
||||
import { DateRange } from "react-day-picker";
|
||||
import { useState } from "react";
|
||||
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||
|
||||
type CalendarFilterButtonProps = {
|
||||
reviewSummary?: ReviewSummary;
|
||||
@@ -24,6 +25,7 @@ export default function CalendarFilterButton({
|
||||
day,
|
||||
updateSelectedDay,
|
||||
}: CalendarFilterButtonProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const selectedDate = useFormattedTimestamp(
|
||||
day == undefined ? 0 : day?.getTime() / 1000 + 1,
|
||||
"%b %-d",
|
||||
@@ -65,20 +67,14 @@ export default function CalendarFilterButton({
|
||||
</>
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer>
|
||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||
<DrawerContent>{content}</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||
<PopoverContent className="w-auto">{content}</PopoverContent>
|
||||
</Popover>
|
||||
<PlatformAwareDialog
|
||||
trigger={trigger}
|
||||
content={content}
|
||||
contentClassName="w-auto"
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Button } from "../ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||
import useSWR from "swr";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
@@ -8,7 +7,6 @@ import { ReviewFilter, ReviewSeverity, ReviewSummary } from "@/types/review";
|
||||
import { getEndOfDayTimestamp } from "@/utils/dateUtil";
|
||||
import { FaCheckCircle, FaFilter, FaRunning } from "react-icons/fa";
|
||||
import { isDesktop, isMobile } from "react-device-detect";
|
||||
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||
import { Switch } from "../ui/switch";
|
||||
import { Label } from "../ui/label";
|
||||
import MobileReviewSettingsDrawer, {
|
||||
@@ -19,6 +17,7 @@ import FilterSwitch from "./FilterSwitch";
|
||||
import { FilterList } from "@/types/filter";
|
||||
import CalendarFilterButton from "./CalendarFilterButton";
|
||||
import { CamerasFilterButton } from "./CamerasFilterButton";
|
||||
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||
|
||||
const REVIEW_FILTERS = [
|
||||
"cameras",
|
||||
@@ -367,28 +366,10 @@ function GeneralFilterButton({
|
||||
/>
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setCurrentLabels(selectedLabels);
|
||||
}
|
||||
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||
<DrawerContent className="max-h-[75dvh] overflow-hidden">
|
||||
{content}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
<PlatformAwareDialog
|
||||
trigger={trigger}
|
||||
content={content}
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
@@ -397,10 +378,7 @@ function GeneralFilterButton({
|
||||
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||
<PopoverContent>{content}</PopoverContent>
|
||||
</Popover>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,38 +6,29 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { DropdownMenuSeparator } from "../ui/dropdown-menu";
|
||||
import { getEndOfDayTimestamp } from "@/utils/dateUtil";
|
||||
import { isDesktop, isMobile } from "react-device-detect";
|
||||
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||
import { Switch } from "../ui/switch";
|
||||
import { Label } from "../ui/label";
|
||||
import FilterSwitch from "./FilterSwitch";
|
||||
import { FilterList } from "@/types/filter";
|
||||
import { CalendarRangeFilterButton } from "./CalendarFilterButton";
|
||||
import { CamerasFilterButton } from "./CamerasFilterButton";
|
||||
import { SearchFilter, SearchSource } from "@/types/search";
|
||||
import {
|
||||
DEFAULT_SEARCH_FILTERS,
|
||||
SearchFilter,
|
||||
SearchFilters,
|
||||
SearchSource,
|
||||
DEFAULT_TIME_RANGE_AFTER,
|
||||
DEFAULT_TIME_RANGE_BEFORE,
|
||||
} from "@/types/search";
|
||||
import { DateRange } from "react-day-picker";
|
||||
import { cn } from "@/lib/utils";
|
||||
import SubFilterIcon from "../icons/SubFilterIcon";
|
||||
import { FaLocationDot } from "react-icons/fa6";
|
||||
import { MdLabel } from "react-icons/md";
|
||||
import SearchSourceIcon from "../icons/SearchSourceIcon";
|
||||
|
||||
const SEARCH_FILTERS = [
|
||||
"cameras",
|
||||
"date",
|
||||
"general",
|
||||
"zone",
|
||||
"sub",
|
||||
"source",
|
||||
] as const;
|
||||
type SearchFilters = (typeof SEARCH_FILTERS)[number];
|
||||
const DEFAULT_REVIEW_FILTERS: SearchFilters[] = [
|
||||
"cameras",
|
||||
"date",
|
||||
"general",
|
||||
"zone",
|
||||
"sub",
|
||||
"source",
|
||||
];
|
||||
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||
import { FaArrowRight, FaClock } from "react-icons/fa";
|
||||
import { useFormattedHour } from "@/hooks/use-date-utils";
|
||||
|
||||
type SearchFilterGroupProps = {
|
||||
className: string;
|
||||
@@ -48,7 +39,7 @@ type SearchFilterGroupProps = {
|
||||
};
|
||||
export default function SearchFilterGroup({
|
||||
className,
|
||||
filters = DEFAULT_REVIEW_FILTERS,
|
||||
filters = DEFAULT_SEARCH_FILTERS,
|
||||
filter,
|
||||
filterList,
|
||||
onUpdateFilter,
|
||||
@@ -182,6 +173,15 @@ export default function SearchFilterGroup({
|
||||
updateSelectedRange={onUpdateSelectedRange}
|
||||
/>
|
||||
)}
|
||||
{filters.includes("time") && (
|
||||
<TimeRangeFilterButton
|
||||
config={config}
|
||||
timeRange={filter?.time_range}
|
||||
updateTimeRange={(time_range) =>
|
||||
onUpdateFilter({ ...filter, time_range })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{filters.includes("zone") && allZones.length > 0 && (
|
||||
<ZoneFilterButton
|
||||
allZones={filterValues.zones}
|
||||
@@ -203,9 +203,9 @@ export default function SearchFilterGroup({
|
||||
{filters.includes("sub") && (
|
||||
<SubFilterButton
|
||||
allSubLabels={allSubLabels}
|
||||
selectedSubLabels={filter?.subLabels}
|
||||
selectedSubLabels={filter?.sub_labels}
|
||||
updateSubLabelFilter={(newSubLabels) =>
|
||||
onUpdateFilter({ ...filter, subLabels: newSubLabels })
|
||||
onUpdateFilter({ ...filter, sub_labels: newSubLabels })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
@@ -291,28 +291,11 @@ function GeneralFilterButton({
|
||||
/>
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setCurrentLabels(selectedLabels);
|
||||
}
|
||||
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||
<DrawerContent className="max-h-[75dvh] overflow-hidden p-4">
|
||||
{content}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
<PlatformAwareDialog
|
||||
trigger={trigger}
|
||||
content={content}
|
||||
contentClassName={isDesktop ? "" : "max-h-[75dvh] overflow-hidden p-4"}
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
@@ -321,10 +304,7 @@ function GeneralFilterButton({
|
||||
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||
<PopoverContent>{content}</PopoverContent>
|
||||
</Popover>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -418,6 +398,186 @@ 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);
|
||||
|
||||
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={cn(
|
||||
"scrollbar-container flex h-auto max-h-[80dvh] flex-col overflow-y-auto overflow-x-hidden",
|
||||
isDesktop ? "w-64" : "w-full gap-2 pt-2",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-3 flex w-full items-center rounded-lg text-secondary-foreground",
|
||||
isDesktop ? "mx-6 gap-2 px-2" : "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-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={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>
|
||||
<DropdownMenuSeparator />
|
||||
</div>
|
||||
<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);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<PlatformAwareDialog
|
||||
trigger={trigger}
|
||||
content={content}
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
setOpen(open);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type ZoneFilterButtonProps = {
|
||||
allZones: string[];
|
||||
selectedZones?: string[];
|
||||
@@ -485,28 +645,10 @@ function ZoneFilterButton({
|
||||
/>
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setCurrentZones(selectedZones);
|
||||
}
|
||||
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||
<DrawerContent className="max-h-[75dvh] overflow-hidden p-4">
|
||||
{content}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
<PlatformAwareDialog
|
||||
trigger={trigger}
|
||||
content={content}
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
@@ -515,10 +657,7 @@ function ZoneFilterButton({
|
||||
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||
<PopoverContent>{content}</PopoverContent>
|
||||
</Popover>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -679,28 +818,10 @@ function SubFilterButton({
|
||||
/>
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setCurrentSubLabels(selectedSubLabels);
|
||||
}
|
||||
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||
<DrawerContent className="max-h-[75dvh] overflow-hidden p-4">
|
||||
{content}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
<PlatformAwareDialog
|
||||
trigger={trigger}
|
||||
content={content}
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
@@ -709,10 +830,7 @@ function SubFilterButton({
|
||||
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||
<PopoverContent>{content}</PopoverContent>
|
||||
</Popover>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -863,32 +981,13 @@ function SearchTypeButton({
|
||||
/>
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||
<DrawerContent className="max-h-[75dvh] overflow-hidden p-4">
|
||||
{content}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
<PlatformAwareDialog
|
||||
trigger={trigger}
|
||||
content={content}
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||
<PopoverContent>{content}</PopoverContent>
|
||||
</Popover>
|
||||
onOpenChange={setOpen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,9 +35,13 @@ import { SaveSearchDialog } from "./SaveSearchDialog";
|
||||
import { DeleteSearchDialog } from "./DeleteSearchDialog";
|
||||
import {
|
||||
convertLocalDateToTimestamp,
|
||||
convertTo12Hour,
|
||||
getIntlDateFormat,
|
||||
isValidTimeRange,
|
||||
} from "@/utils/dateUtil";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
|
||||
type InputWithTagsProps = {
|
||||
filters: SearchFilter;
|
||||
@@ -56,6 +60,10 @@ export default function InputWithTags({
|
||||
setSearch,
|
||||
allSuggestions,
|
||||
}: InputWithTagsProps) {
|
||||
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
const [inputValue, setInputValue] = useState(search || "");
|
||||
const [currentFilterType, setCurrentFilterType] = useState<FilterType | null>(
|
||||
null,
|
||||
@@ -180,7 +188,11 @@ export default function InputWithTags({
|
||||
|
||||
const createFilter = useCallback(
|
||||
(type: FilterType, value: string) => {
|
||||
if (allSuggestions[type as FilterType]?.includes(value)) {
|
||||
if (
|
||||
allSuggestions[type as FilterType]?.includes(value) ||
|
||||
type == "before" ||
|
||||
type == "after"
|
||||
) {
|
||||
const newFilters = { ...filters };
|
||||
let timestamp = 0;
|
||||
|
||||
@@ -222,6 +234,26 @@ export default function InputWithTags({
|
||||
newFilters[type] = timestamp / 1000;
|
||||
}
|
||||
break;
|
||||
case "time_range":
|
||||
if (!value.includes(",")) {
|
||||
toast.error(
|
||||
"The correct format is after,before. Example: 15:00,18:00.",
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidTimeRange(value)) {
|
||||
toast.error("Time range is not valid.", {
|
||||
position: "top-center",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
newFilters[type] = value;
|
||||
break;
|
||||
case "search_type":
|
||||
if (!newFilters.search_type) newFilters.search_type = [];
|
||||
if (
|
||||
@@ -256,6 +288,30 @@ export default function InputWithTags({
|
||||
[filters, setFilters, allSuggestions],
|
||||
);
|
||||
|
||||
function formatFilterValues(
|
||||
filterType: string,
|
||||
filterValues: number | string,
|
||||
): string {
|
||||
if (filterType === "before" || filterType === "after") {
|
||||
return new Date(
|
||||
(filterType === "before"
|
||||
? (filterValues as number) + 1
|
||||
: (filterValues as number)) * 1000,
|
||||
).toLocaleDateString(window.navigator?.language || "en-US");
|
||||
} else if (filterType === "time_range") {
|
||||
const [startTime, endTime] = (filterValues as string).split(",");
|
||||
return `${
|
||||
config?.ui.time_format === "24hour"
|
||||
? startTime
|
||||
: convertTo12Hour(startTime)
|
||||
} - ${
|
||||
config?.ui.time_format === "24hour" ? endTime : convertTo12Hour(endTime)
|
||||
}`;
|
||||
} else {
|
||||
return filterValues as string;
|
||||
}
|
||||
}
|
||||
|
||||
// handlers
|
||||
|
||||
const handleFilterCreation = useCallback(
|
||||
@@ -303,11 +359,7 @@ export default function InputWithTags({
|
||||
];
|
||||
|
||||
// Check if filter type is valid
|
||||
if (
|
||||
filterType in allSuggestions ||
|
||||
filterType === "before" ||
|
||||
filterType === "after"
|
||||
) {
|
||||
if (filterType in allSuggestions) {
|
||||
setCurrentFilterType(filterType);
|
||||
|
||||
if (filterType === "before" || filterType === "after") {
|
||||
@@ -604,16 +656,8 @@ export default function InputWithTags({
|
||||
key={filterType}
|
||||
className="inline-flex items-center whitespace-nowrap rounded-full bg-green-100 px-2 py-0.5 text-sm capitalize text-green-800"
|
||||
>
|
||||
{filterType}:
|
||||
{filterType === "before" || filterType === "after"
|
||||
? new Date(
|
||||
(filterType === "before"
|
||||
? (filterValues as number) + 1
|
||||
: (filterValues as number)) * 1000,
|
||||
).toLocaleDateString(
|
||||
window.navigator?.language || "en-US",
|
||||
)
|
||||
: filterValues}
|
||||
{filterType.replaceAll("_", " ")}:{" "}
|
||||
{formatFilterValues(filterType, filterValues)}
|
||||
<button
|
||||
onClick={() =>
|
||||
removeFilter(
|
||||
|
||||
44
web/src/components/overlay/dialog/PlatformAwareDialog.tsx
Normal file
44
web/src/components/overlay/dialog/PlatformAwareDialog.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { isMobile } from "react-device-detect";
|
||||
|
||||
type PlatformAwareDialogProps = {
|
||||
trigger: JSX.Element;
|
||||
content: JSX.Element;
|
||||
triggerClassName?: string;
|
||||
contentClassName?: string;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
};
|
||||
export default function PlatformAwareDialog({
|
||||
trigger,
|
||||
content,
|
||||
triggerClassName = "",
|
||||
contentClassName = "",
|
||||
open,
|
||||
onOpenChange,
|
||||
}: PlatformAwareDialogProps) {
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={onOpenChange}>
|
||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||
<DrawerContent className="max-h-[75dvh] overflow-hidden p-4">
|
||||
{content}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={onOpenChange}>
|
||||
<PopoverTrigger asChild className={triggerClassName}>
|
||||
{trigger}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className={contentClassName}>{content}</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user