Separate out filters and clean up detail pane

This commit is contained in:
Nicolas Mowen 2024-09-09 16:43:33 -06:00
parent 6c2d9b15a6
commit 75bab2d601
5 changed files with 188 additions and 57 deletions

View File

@ -17,13 +17,16 @@ import { CamerasFilterButton } from "./CamerasFilterButton";
import { SearchFilter, SearchSource } 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";
const SEARCH_FILTERS = ["cameras", "date", "general", "sub"] as const;
const SEARCH_FILTERS = ["cameras", "date", "general", "zone", "sub"] as const;
type SearchFilters = (typeof SEARCH_FILTERS)[number];
const DEFAULT_REVIEW_FILTERS: SearchFilters[] = [
"cameras",
"date",
"general",
"zone",
"sub",
];
@ -172,22 +175,26 @@ export default function SearchFilterGroup({
<GeneralFilterButton
allLabels={filterValues.labels}
selectedLabels={filter?.labels}
allZones={filterValues.zones}
selectedZones={filter?.zones}
selectedSearchSources={
filter?.search_type ?? ["thumbnail", "description"]
}
updateLabelFilter={(newLabels) => {
onUpdateFilter({ ...filter, labels: newLabels });
}}
updateZoneFilter={(newZones) =>
onUpdateFilter({ ...filter, zones: newZones })
}
updateSearchSourceFilter={(newSearchSource) =>
onUpdateFilter({ ...filter, search_type: newSearchSource })
}
/>
)}
{filters.includes("zone") && allZones.length > 0 && (
<ZoneFilterButton
allZones={filterValues.zones}
selectedZones={filter?.zones}
updateZoneFilter={(newZones) =>
onUpdateFilter({ ...filter, zones: newZones })
}
/>
)}
{filters.includes("sub") && (
<SubFilterButton
allSubLabels={allSubLabels}
@ -204,30 +211,21 @@ export default function SearchFilterGroup({
type GeneralFilterButtonProps = {
allLabels: string[];
selectedLabels: string[] | undefined;
allZones: string[];
selectedZones?: string[];
selectedSearchSources: SearchSource[];
updateLabelFilter: (labels: string[] | undefined) => void;
updateZoneFilter: (zones: string[] | undefined) => void;
updateSearchSourceFilter: (sources: SearchSource[]) => void;
};
function GeneralFilterButton({
allLabels,
selectedLabels,
allZones,
selectedZones,
selectedSearchSources,
updateLabelFilter,
updateZoneFilter,
updateSearchSourceFilter,
}: GeneralFilterButtonProps) {
const [open, setOpen] = useState(false);
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
selectedLabels,
);
const [currentZones, setCurrentZones] = useState<string[] | undefined>(
selectedZones,
);
const [currentSearchSources, setCurrentSearchSources] = useState<
SearchSource[]
>(selectedSearchSources);
@ -235,16 +233,14 @@ function GeneralFilterButton({
const trigger = (
<Button
size="sm"
variant={
selectedLabels?.length || selectedZones?.length ? "select" : "default"
}
variant={selectedLabels?.length ? "select" : "default"}
className="flex items-center gap-2 capitalize"
>
<FaFilter
className={`${selectedLabels?.length || selectedZones?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
className={`${selectedLabels?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
/>
<div
className={`hidden md:block ${selectedLabels?.length || selectedZones?.length ? "text-selected-foreground" : "text-primary"}`}
className={`hidden md:block ${selectedLabels?.length ? "text-selected-foreground" : "text-primary"}`}
>
Filter
</div>
@ -255,13 +251,8 @@ function GeneralFilterButton({
allLabels={allLabels}
selectedLabels={selectedLabels}
currentLabels={currentLabels}
allZones={allZones}
selectedZones={selectedZones}
currentZones={currentZones}
selectedSearchSources={selectedSearchSources}
currentSearchSources={currentSearchSources}
setCurrentZones={setCurrentZones}
updateZoneFilter={updateZoneFilter}
setCurrentLabels={setCurrentLabels}
updateLabelFilter={updateLabelFilter}
setCurrentSearchSources={setCurrentSearchSources}
@ -311,15 +302,10 @@ type GeneralFilterContentProps = {
allLabels: string[];
selectedLabels: string[] | undefined;
currentLabels: string[] | undefined;
allZones?: string[];
selectedZones?: string[];
currentZones?: string[];
selectedSearchSources: SearchSource[];
currentSearchSources: SearchSource[];
updateLabelFilter: (labels: string[] | undefined) => void;
setCurrentLabels: (labels: string[] | undefined) => void;
updateZoneFilter?: (zones: string[] | undefined) => void;
setCurrentZones?: (zones: string[] | undefined) => void;
setCurrentSearchSources: (sources: SearchSource[]) => void;
updateSearchSourceFilter: (sources: SearchSource[]) => void;
onClose: () => void;
@ -328,15 +314,10 @@ export function GeneralFilterContent({
allLabels,
selectedLabels,
currentLabels,
allZones,
selectedZones,
currentZones,
selectedSearchSources,
currentSearchSources,
updateLabelFilter,
setCurrentLabels,
updateZoneFilter,
setCurrentZones,
setCurrentSearchSources,
updateSearchSourceFilter,
onClose,
@ -436,7 +417,137 @@ export function GeneralFilterContent({
/>
))}
</div>
</div>
<DropdownMenuSeparator />
<div className="flex items-center justify-evenly p-2">
<Button
variant="select"
onClick={() => {
if (selectedLabels != currentLabels) {
updateLabelFilter(currentLabels);
}
if (selectedSearchSources != currentSearchSources) {
updateSearchSourceFilter(currentSearchSources);
}
onClose();
}}
>
Apply
</Button>
<Button
onClick={() => {
setCurrentLabels(undefined);
updateLabelFilter(undefined);
}}
>
Reset
</Button>
</div>
</>
);
}
type ZoneFilterButtonProps = {
allZones: string[];
selectedZones?: string[];
updateZoneFilter: (zones: string[] | undefined) => void;
};
function ZoneFilterButton({
allZones,
selectedZones,
updateZoneFilter,
}: ZoneFilterButtonProps) {
const [open, setOpen] = useState(false);
const [currentZones, setCurrentZones] = useState<string[] | undefined>(
selectedZones,
);
const trigger = (
<Button
size="sm"
variant={selectedZones?.length ? "select" : "default"}
className="flex items-center gap-2 capitalize"
>
<FaLocationDot
className={`${selectedZones?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
/>
<div
className={`hidden md:block ${selectedZones?.length ? "text-selected-foreground" : "text-primary"}`}
>
Filter
</div>
</Button>
);
const content = (
<ZoneFilterContent
allZones={allZones}
selectedZones={selectedZones}
currentZones={currentZones}
setCurrentZones={setCurrentZones}
updateZoneFilter={updateZoneFilter}
onClose={() => setOpen(false)}
/>
);
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">
{content}
</DrawerContent>
</Drawer>
);
}
return (
<Popover
open={open}
onOpenChange={(open) => {
if (!open) {
setCurrentZones(selectedZones);
}
setOpen(open);
}}
>
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
<PopoverContent>{content}</PopoverContent>
</Popover>
);
}
type ZoneFilterContentProps = {
allZones?: string[];
selectedZones?: string[];
currentZones?: string[];
updateZoneFilter?: (zones: string[] | undefined) => void;
setCurrentZones?: (zones: string[] | undefined) => void;
onClose: () => void;
};
export function ZoneFilterContent({
allZones,
selectedZones,
currentZones,
updateZoneFilter,
setCurrentZones,
onClose,
}: ZoneFilterContentProps) {
return (
<>
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
{allZones && setCurrentZones && (
<>
<DropdownMenuSeparator />
@ -495,18 +606,10 @@ export function GeneralFilterContent({
<Button
variant="select"
onClick={() => {
if (selectedLabels != currentLabels) {
updateLabelFilter(currentLabels);
}
if (updateZoneFilter && selectedZones != currentZones) {
updateZoneFilter(currentZones);
}
if (selectedSearchSources != currentSearchSources) {
updateSearchSourceFilter(currentSearchSources);
}
onClose();
}}
>
@ -514,9 +617,7 @@ export function GeneralFilterContent({
</Button>
<Button
onClick={() => {
setCurrentLabels(undefined);
setCurrentZones?.(undefined);
updateLabelFilter(undefined);
}}
>
Reset
@ -547,7 +648,7 @@ function SubFilterButton({
variant={selectedSubLabels?.length ? "select" : "default"}
className="flex items-center gap-2 capitalize"
>
<FaFilter
<SubFilterIcon
className={`${selectedSubLabels?.length || selectedSubLabels?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
/>
<div

View File

@ -0,0 +1,25 @@
import { forwardRef } from "react";
import { cn } from "@/lib/utils";
import { FaCog, FaFilter } from "react-icons/fa";
type SubFilterIconProps = {
className?: string;
onClick?: () => void;
};
const SubFilterIcon = forwardRef<HTMLDivElement, SubFilterIconProps>(
({ className, onClick }, ref) => {
return (
<div
ref={ref}
className={cn("relative flex items-center", className)}
onClick={onClick}
>
<FaFilter className="size-full" />
<FaCog className="absolute size-3 translate-x-3 translate-y-3/4" />
</div>
);
},
);
export default SubFilterIcon;

View File

@ -114,12 +114,15 @@ export default function SearchDetailDialog({
<div className="flex flex-row items-center gap-2 text-sm capitalize">
{getIconForLabel(search.label, "size-4 text-primary")}
{search.label}
{search.sub_label && ` (${search.sub_label})`}
</div>
</div>
<div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">Score</div>
<div className="text-sm">
{Math.round(search.score * 100)}%
{Math.round(search.data.top_score * 100)}%
{search.sub_label &&
` (${Math.round((search.data.sub_label_score ?? 0) * 100)}%)`}
</div>
</div>
<div className="flex flex-col gap-1.5">

View File

@ -13,6 +13,16 @@ export type SearchResult = {
zones: string[];
search_source: SearchSource;
search_distance: number;
data: {
top_score: number;
score: number;
sub_label_score?: number;
region: number[];
box: number[];
area: number;
ratio: number;
type: "object" | "audio" | "manual";
};
};
export type SearchFilter = {

View File

@ -16,15 +16,7 @@ import { Preview } from "@/types/preview";
import { SearchFilter, SearchResult } from "@/types/search";
import { useCallback, useMemo, useState } from "react";
import { isMobileOnly } from "react-device-detect";
import {
LuExternalLink,
LuImage,
LuSearchCheck,
LuSearchX,
LuText,
LuXCircle,
} from "react-icons/lu";
import { Link } from "react-router-dom";
import { LuImage, LuSearchX, LuText, LuXCircle } from "react-icons/lu";
import useSWR from "swr";
type SearchViewProps = {
@ -153,7 +145,7 @@ export default function SearchView({
{hasExistingSearch && (
<SearchFilterGroup
className={cn("", isMobileOnly && "w-full")}
className={cn("", isMobileOnly && "w-full justify-between")}
filter={searchFilter}
onUpdateFilter={onUpdateFilter}
/>