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 { SearchFilter, SearchSource } from "@/types/search";
import { DateRange } from "react-day-picker"; import { DateRange } from "react-day-picker";
import { cn } from "@/lib/utils"; 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]; type SearchFilters = (typeof SEARCH_FILTERS)[number];
const DEFAULT_REVIEW_FILTERS: SearchFilters[] = [ const DEFAULT_REVIEW_FILTERS: SearchFilters[] = [
"cameras", "cameras",
"date", "date",
"general", "general",
"zone",
"sub", "sub",
]; ];
@ -172,22 +175,26 @@ export default function SearchFilterGroup({
<GeneralFilterButton <GeneralFilterButton
allLabels={filterValues.labels} allLabels={filterValues.labels}
selectedLabels={filter?.labels} selectedLabels={filter?.labels}
allZones={filterValues.zones}
selectedZones={filter?.zones}
selectedSearchSources={ selectedSearchSources={
filter?.search_type ?? ["thumbnail", "description"] filter?.search_type ?? ["thumbnail", "description"]
} }
updateLabelFilter={(newLabels) => { updateLabelFilter={(newLabels) => {
onUpdateFilter({ ...filter, labels: newLabels }); onUpdateFilter({ ...filter, labels: newLabels });
}} }}
updateZoneFilter={(newZones) =>
onUpdateFilter({ ...filter, zones: newZones })
}
updateSearchSourceFilter={(newSearchSource) => updateSearchSourceFilter={(newSearchSource) =>
onUpdateFilter({ ...filter, search_type: 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") && ( {filters.includes("sub") && (
<SubFilterButton <SubFilterButton
allSubLabels={allSubLabels} allSubLabels={allSubLabels}
@ -204,30 +211,21 @@ export default function SearchFilterGroup({
type GeneralFilterButtonProps = { type GeneralFilterButtonProps = {
allLabels: string[]; allLabels: string[];
selectedLabels: string[] | undefined; selectedLabels: string[] | undefined;
allZones: string[];
selectedZones?: string[];
selectedSearchSources: SearchSource[]; selectedSearchSources: SearchSource[];
updateLabelFilter: (labels: string[] | undefined) => void; updateLabelFilter: (labels: string[] | undefined) => void;
updateZoneFilter: (zones: string[] | undefined) => void;
updateSearchSourceFilter: (sources: SearchSource[]) => void; updateSearchSourceFilter: (sources: SearchSource[]) => void;
}; };
function GeneralFilterButton({ function GeneralFilterButton({
allLabels, allLabels,
selectedLabels, selectedLabels,
allZones,
selectedZones,
selectedSearchSources, selectedSearchSources,
updateLabelFilter, updateLabelFilter,
updateZoneFilter,
updateSearchSourceFilter, updateSearchSourceFilter,
}: GeneralFilterButtonProps) { }: GeneralFilterButtonProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>( const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
selectedLabels, selectedLabels,
); );
const [currentZones, setCurrentZones] = useState<string[] | undefined>(
selectedZones,
);
const [currentSearchSources, setCurrentSearchSources] = useState< const [currentSearchSources, setCurrentSearchSources] = useState<
SearchSource[] SearchSource[]
>(selectedSearchSources); >(selectedSearchSources);
@ -235,16 +233,14 @@ function GeneralFilterButton({
const trigger = ( const trigger = (
<Button <Button
size="sm" size="sm"
variant={ variant={selectedLabels?.length ? "select" : "default"}
selectedLabels?.length || selectedZones?.length ? "select" : "default"
}
className="flex items-center gap-2 capitalize" className="flex items-center gap-2 capitalize"
> >
<FaFilter <FaFilter
className={`${selectedLabels?.length || selectedZones?.length ? "text-selected-foreground" : "text-secondary-foreground"}`} className={`${selectedLabels?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
/> />
<div <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 Filter
</div> </div>
@ -255,13 +251,8 @@ function GeneralFilterButton({
allLabels={allLabels} allLabels={allLabels}
selectedLabels={selectedLabels} selectedLabels={selectedLabels}
currentLabels={currentLabels} currentLabels={currentLabels}
allZones={allZones}
selectedZones={selectedZones}
currentZones={currentZones}
selectedSearchSources={selectedSearchSources} selectedSearchSources={selectedSearchSources}
currentSearchSources={currentSearchSources} currentSearchSources={currentSearchSources}
setCurrentZones={setCurrentZones}
updateZoneFilter={updateZoneFilter}
setCurrentLabels={setCurrentLabels} setCurrentLabels={setCurrentLabels}
updateLabelFilter={updateLabelFilter} updateLabelFilter={updateLabelFilter}
setCurrentSearchSources={setCurrentSearchSources} setCurrentSearchSources={setCurrentSearchSources}
@ -311,15 +302,10 @@ type GeneralFilterContentProps = {
allLabels: string[]; allLabels: string[];
selectedLabels: string[] | undefined; selectedLabels: string[] | undefined;
currentLabels: string[] | undefined; currentLabels: string[] | undefined;
allZones?: string[];
selectedZones?: string[];
currentZones?: string[];
selectedSearchSources: SearchSource[]; selectedSearchSources: SearchSource[];
currentSearchSources: SearchSource[]; currentSearchSources: SearchSource[];
updateLabelFilter: (labels: string[] | undefined) => void; updateLabelFilter: (labels: string[] | undefined) => void;
setCurrentLabels: (labels: string[] | undefined) => void; setCurrentLabels: (labels: string[] | undefined) => void;
updateZoneFilter?: (zones: string[] | undefined) => void;
setCurrentZones?: (zones: string[] | undefined) => void;
setCurrentSearchSources: (sources: SearchSource[]) => void; setCurrentSearchSources: (sources: SearchSource[]) => void;
updateSearchSourceFilter: (sources: SearchSource[]) => void; updateSearchSourceFilter: (sources: SearchSource[]) => void;
onClose: () => void; onClose: () => void;
@ -328,15 +314,10 @@ export function GeneralFilterContent({
allLabels, allLabels,
selectedLabels, selectedLabels,
currentLabels, currentLabels,
allZones,
selectedZones,
currentZones,
selectedSearchSources, selectedSearchSources,
currentSearchSources, currentSearchSources,
updateLabelFilter, updateLabelFilter,
setCurrentLabels, setCurrentLabels,
updateZoneFilter,
setCurrentZones,
setCurrentSearchSources, setCurrentSearchSources,
updateSearchSourceFilter, updateSearchSourceFilter,
onClose, onClose,
@ -436,7 +417,137 @@ export function GeneralFilterContent({
/> />
))} ))}
</div> </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 && ( {allZones && setCurrentZones && (
<> <>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
@ -495,18 +606,10 @@ export function GeneralFilterContent({
<Button <Button
variant="select" variant="select"
onClick={() => { onClick={() => {
if (selectedLabels != currentLabels) {
updateLabelFilter(currentLabels);
}
if (updateZoneFilter && selectedZones != currentZones) { if (updateZoneFilter && selectedZones != currentZones) {
updateZoneFilter(currentZones); updateZoneFilter(currentZones);
} }
if (selectedSearchSources != currentSearchSources) {
updateSearchSourceFilter(currentSearchSources);
}
onClose(); onClose();
}} }}
> >
@ -514,9 +617,7 @@ export function GeneralFilterContent({
</Button> </Button>
<Button <Button
onClick={() => { onClick={() => {
setCurrentLabels(undefined);
setCurrentZones?.(undefined); setCurrentZones?.(undefined);
updateLabelFilter(undefined);
}} }}
> >
Reset Reset
@ -547,7 +648,7 @@ function SubFilterButton({
variant={selectedSubLabels?.length ? "select" : "default"} variant={selectedSubLabels?.length ? "select" : "default"}
className="flex items-center gap-2 capitalize" className="flex items-center gap-2 capitalize"
> >
<FaFilter <SubFilterIcon
className={`${selectedSubLabels?.length || selectedSubLabels?.length ? "text-selected-foreground" : "text-secondary-foreground"}`} className={`${selectedSubLabels?.length || selectedSubLabels?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
/> />
<div <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"> <div className="flex flex-row items-center gap-2 text-sm capitalize">
{getIconForLabel(search.label, "size-4 text-primary")} {getIconForLabel(search.label, "size-4 text-primary")}
{search.label} {search.label}
{search.sub_label && ` (${search.sub_label})`}
</div> </div>
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">Score</div> <div className="text-sm text-primary/40">Score</div>
<div className="text-sm"> <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> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">

View File

@ -13,6 +13,16 @@ export type SearchResult = {
zones: string[]; zones: string[];
search_source: SearchSource; search_source: SearchSource;
search_distance: number; 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 = { export type SearchFilter = {

View File

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