mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-22 08:57:20 -06:00
Explore pane infinite loading (#13738)
* swr for infinite loading * search detail language change * drawer padding * spacing * center calendar * padding * catch error * use limit const
This commit is contained in:
parent
088a0fb4a5
commit
2a66923524
@ -90,7 +90,7 @@ export function CamerasFilterButton({
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden p-4">
|
<div className="scrollbar-container flex h-auto max-h-[80dvh] flex-col gap-2 overflow-y-auto overflow-x-hidden p-4">
|
||||||
<FilterSwitch
|
<FilterSwitch
|
||||||
isChecked={allCamerasSelected}
|
isChecked={allCamerasSelected}
|
||||||
label="All Cameras"
|
label="All Cameras"
|
||||||
@ -106,12 +106,12 @@ export function CamerasFilterButton({
|
|||||||
/>
|
/>
|
||||||
{groups.length > 0 && (
|
{groups.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator className="mt-2" />
|
<DropdownMenuSeparator />
|
||||||
{groups.map(([name, conf]) => {
|
{groups.map(([name, conf]) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={name}
|
key={name}
|
||||||
className="w-full cursor-pointer rounded-lg px-2 py-1.5 text-sm capitalize text-primary hover:bg-muted"
|
className="w-full cursor-pointer rounded-lg px-2 py-0.5 text-sm capitalize text-primary hover:bg-muted"
|
||||||
onClick={() => setCurrentCameras([...conf.cameras])}
|
onClick={() => setCurrentCameras([...conf.cameras])}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
@ -120,7 +120,7 @@ export function CamerasFilterButton({
|
|||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuSeparator className="my-2" />
|
<DropdownMenuSeparator />
|
||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex flex-col gap-2.5">
|
||||||
{allCameras.map((item) => (
|
{allCameras.map((item) => (
|
||||||
<FilterSwitch
|
<FilterSwitch
|
||||||
@ -158,7 +158,7 @@ export function CamerasFilterButton({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenuSeparator className="my-2" />
|
<DropdownMenuSeparator />
|
||||||
<div className="flex items-center justify-evenly p-2">
|
<div className="flex items-center justify-evenly p-2">
|
||||||
<Button
|
<Button
|
||||||
variant="select"
|
variant="select"
|
||||||
|
@ -5,7 +5,7 @@ import { FrigateConfig } from "@/types/frigateConfig";
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { DropdownMenuSeparator } from "../ui/dropdown-menu";
|
import { DropdownMenuSeparator } from "../ui/dropdown-menu";
|
||||||
import { getEndOfDayTimestamp } from "@/utils/dateUtil";
|
import { getEndOfDayTimestamp } from "@/utils/dateUtil";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isDesktop, isMobile } from "react-device-detect";
|
||||||
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||||
import { Switch } from "../ui/switch";
|
import { Switch } from "../ui/switch";
|
||||||
import { Label } from "../ui/label";
|
import { Label } from "../ui/label";
|
||||||
@ -309,7 +309,7 @@ function GeneralFilterButton({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||||
<DrawerContent className="max-h-[75dvh] overflow-hidden">
|
<DrawerContent className="max-h-[75dvh] overflow-hidden p-4">
|
||||||
{content}
|
{content}
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
@ -503,7 +503,7 @@ function ZoneFilterButton({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||||
<DrawerContent className="max-h-[75dvh] overflow-hidden">
|
<DrawerContent className="max-h-[75dvh] overflow-hidden p-4">
|
||||||
{content}
|
{content}
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
@ -548,7 +548,7 @@ export function ZoneFilterContent({
|
|||||||
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
|
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
|
||||||
{allZones && setCurrentZones && (
|
{allZones && setCurrentZones && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
{isDesktop && <DropdownMenuSeparator />}
|
||||||
<div className="mb-5 mt-2.5 flex items-center justify-between">
|
<div className="mb-5 mt-2.5 flex items-center justify-between">
|
||||||
<Label
|
<Label
|
||||||
className="mx-2 cursor-pointer text-primary"
|
className="mx-2 cursor-pointer text-primary"
|
||||||
@ -599,7 +599,7 @@ export function ZoneFilterContent({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenuSeparator />
|
{isDesktop && <DropdownMenuSeparator />}
|
||||||
<div className="flex items-center justify-evenly p-2">
|
<div className="flex items-center justify-evenly p-2">
|
||||||
<Button
|
<Button
|
||||||
variant="select"
|
variant="select"
|
||||||
@ -697,7 +697,7 @@ function SubFilterButton({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||||
<DrawerContent className="max-h-[75dvh] overflow-hidden">
|
<DrawerContent className="max-h-[75dvh] overflow-hidden p-4">
|
||||||
{content}
|
{content}
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
@ -788,7 +788,7 @@ export function SubFilterContent({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenuSeparator />
|
{isDesktop && <DropdownMenuSeparator />}
|
||||||
<div className="flex items-center justify-evenly p-2">
|
<div className="flex items-center justify-evenly p-2">
|
||||||
<Button
|
<Button
|
||||||
variant="select"
|
variant="select"
|
||||||
@ -877,7 +877,7 @@ function SearchTypeButton({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||||
<DrawerContent className="max-h-[75dvh] overflow-hidden">
|
<DrawerContent className="max-h-[75dvh] overflow-hidden p-4">
|
||||||
{content}
|
{content}
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
@ -956,7 +956,7 @@ export function SearchTypeContent({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenuSeparator />
|
{isDesktop && <DropdownMenuSeparator />}
|
||||||
<div className="flex items-center justify-evenly p-2">
|
<div className="flex items-center justify-evenly p-2">
|
||||||
<Button
|
<Button
|
||||||
variant="select"
|
variant="select"
|
||||||
|
@ -193,7 +193,7 @@ export default function ObjectLifecycle({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (eventSequence) {
|
if (eventSequence && eventSequence.length > 0) {
|
||||||
setTimeIndex(eventSequence?.[current].timestamp);
|
setTimeIndex(eventSequence?.[current].timestamp);
|
||||||
handleSetBox(eventSequence?.[current].data.box ?? []);
|
handleSetBox(eventSequence?.[current].data.box ?? []);
|
||||||
setLifecycleZones(eventSequence?.[current].data.zones);
|
setLifecycleZones(eventSequence?.[current].data.zones);
|
||||||
|
@ -237,7 +237,7 @@ function ObjectDetailsTab({
|
|||||||
const [desc, setDesc] = useState(search?.data.description);
|
const [desc, setDesc] = useState(search?.data.description);
|
||||||
|
|
||||||
// we have to make sure the current selected search item stays in sync
|
// we have to make sure the current selected search item stays in sync
|
||||||
useEffect(() => setDesc(search?.data.description), [search]);
|
useEffect(() => setDesc(search?.data.description ?? ""), [search]);
|
||||||
|
|
||||||
const formattedDate = useFormattedTimestamp(
|
const formattedDate = useFormattedTimestamp(
|
||||||
search?.start_time ?? 0,
|
search?.start_time ?? 0,
|
||||||
@ -351,7 +351,7 @@ function ObjectDetailsTab({
|
|||||||
<div className="text-sm text-primary/40">Description</div>
|
<div className="text-sm text-primary/40">Description</div>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="md:h-64"
|
className="md:h-64"
|
||||||
placeholder="Description of the event"
|
placeholder="Description of the tracked object"
|
||||||
value={desc}
|
value={desc}
|
||||||
onChange={(e) => setDesc(e.target.value)}
|
onChange={(e) => setDesc(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
@ -315,7 +315,7 @@ export function DateRangePicker({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex py-2">
|
<div className="flex flex-row items-start justify-center py-2">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex flex-col items-center justify-end gap-2 px-3 pb-4 lg:flex-row lg:items-start lg:pb-0">
|
<div className="flex flex-col items-center justify-end gap-2 px-3 pb-4 lg:flex-row lg:items-start lg:pb-0">
|
||||||
|
@ -3,12 +3,15 @@ import { useCameraPreviews } from "@/hooks/use-camera-previews";
|
|||||||
import { useOverlayState, useSearchEffect } from "@/hooks/use-overlay-state";
|
import { useOverlayState, useSearchEffect } from "@/hooks/use-overlay-state";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { RecordingStartingPoint } from "@/types/record";
|
import { RecordingStartingPoint } from "@/types/record";
|
||||||
import { SearchFilter, SearchResult } from "@/types/search";
|
import { SearchFilter, SearchQuery, SearchResult } from "@/types/search";
|
||||||
import { TimeRange } from "@/types/timeline";
|
import { TimeRange } from "@/types/timeline";
|
||||||
import { RecordingView } from "@/views/recording/RecordingView";
|
import { RecordingView } from "@/views/recording/RecordingView";
|
||||||
import SearchView from "@/views/search/SearchView";
|
import SearchView from "@/views/search/SearchView";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import useSWRInfinite from "swr/infinite";
|
||||||
|
|
||||||
|
const API_LIMIT = 25;
|
||||||
|
|
||||||
export default function Explore() {
|
export default function Explore() {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config", {
|
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||||
@ -61,7 +64,7 @@ export default function Explore() {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [search]);
|
}, [search]);
|
||||||
|
|
||||||
const searchQuery = useMemo(() => {
|
const searchQuery: SearchQuery = useMemo(() => {
|
||||||
if (similaritySearch) {
|
if (similaritySearch) {
|
||||||
return [
|
return [
|
||||||
"events/search",
|
"events/search",
|
||||||
@ -107,7 +110,8 @@ export default function Explore() {
|
|||||||
before: searchSearchParams["before"],
|
before: searchSearchParams["before"],
|
||||||
after: searchSearchParams["after"],
|
after: searchSearchParams["after"],
|
||||||
search_type: searchSearchParams["search_type"],
|
search_type: searchSearchParams["search_type"],
|
||||||
limit: Object.keys(searchSearchParams).length == 0 ? 20 : null,
|
limit:
|
||||||
|
Object.keys(searchSearchParams).length == 0 ? API_LIMIT : undefined,
|
||||||
in_progress: 0,
|
in_progress: 0,
|
||||||
include_thumbnails: 0,
|
include_thumbnails: 0,
|
||||||
},
|
},
|
||||||
@ -117,8 +121,66 @@ export default function Explore() {
|
|||||||
return null;
|
return null;
|
||||||
}, [searchTerm, searchSearchParams, similaritySearch]);
|
}, [searchTerm, searchSearchParams, similaritySearch]);
|
||||||
|
|
||||||
const { data: searchResults, isLoading } =
|
// paging
|
||||||
useSWR<SearchResult[]>(searchQuery);
|
|
||||||
|
const getKey = (
|
||||||
|
pageIndex: number,
|
||||||
|
previousPageData: SearchResult[] | null,
|
||||||
|
): SearchQuery => {
|
||||||
|
if (previousPageData && !previousPageData.length) return null; // reached the end
|
||||||
|
if (!searchQuery) return null;
|
||||||
|
|
||||||
|
const [url, params] = searchQuery;
|
||||||
|
|
||||||
|
// If it's not the first page, use the last item's start_time as the 'before' parameter
|
||||||
|
if (pageIndex > 0 && previousPageData) {
|
||||||
|
const lastDate = previousPageData[previousPageData.length - 1].start_time;
|
||||||
|
return [
|
||||||
|
url,
|
||||||
|
{ ...params, before: lastDate.toString(), limit: API_LIMIT },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the first page, use the original params
|
||||||
|
return [url, { ...params, limit: API_LIMIT }];
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, size, setSize, isValidating } = useSWRInfinite<SearchResult[]>(
|
||||||
|
getKey,
|
||||||
|
{
|
||||||
|
revalidateFirstPage: false,
|
||||||
|
revalidateAll: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchResults = useMemo(
|
||||||
|
() => (data ? ([] as SearchResult[]).concat(...data) : []),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
const isLoadingInitialData = !data && !isValidating;
|
||||||
|
const isLoadingMore =
|
||||||
|
isLoadingInitialData ||
|
||||||
|
(size > 0 && data && typeof data[size - 1] === "undefined");
|
||||||
|
const isEmpty = data?.[0]?.length === 0;
|
||||||
|
const isReachingEnd =
|
||||||
|
isEmpty || (data && data[data.length - 1]?.length < API_LIMIT);
|
||||||
|
|
||||||
|
const loadMore = useCallback(() => {
|
||||||
|
if (!isReachingEnd && !isLoadingMore) {
|
||||||
|
if (searchQuery) {
|
||||||
|
const [url] = searchQuery;
|
||||||
|
|
||||||
|
// for chroma, only load 100 results for description and similarity
|
||||||
|
if (url === "events/search" && searchResults.length >= 100) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSize(size + 1);
|
||||||
|
}
|
||||||
|
}, [isReachingEnd, isLoadingMore, setSize, size, searchResults, searchQuery]);
|
||||||
|
|
||||||
|
// previews
|
||||||
|
|
||||||
const previewTimeRange = useMemo<TimeRange>(() => {
|
const previewTimeRange = useMemo<TimeRange>(() => {
|
||||||
if (!searchResults) {
|
if (!searchResults) {
|
||||||
@ -212,11 +274,13 @@ export default function Explore() {
|
|||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
searchFilter={searchFilter}
|
searchFilter={searchFilter}
|
||||||
searchResults={searchResults}
|
searchResults={searchResults}
|
||||||
isLoading={isLoading}
|
isLoading={(isLoadingInitialData || isLoadingMore) ?? true}
|
||||||
setSearch={setSearch}
|
setSearch={setSearch}
|
||||||
setSimilaritySearch={(search) => setSearch(`similarity:${search.id}`)}
|
setSimilaritySearch={(search) => setSearch(`similarity:${search.id}`)}
|
||||||
onUpdateFilter={setSearchFilter}
|
onUpdateFilter={setSearchFilter}
|
||||||
onOpenSearch={onOpenSearch}
|
onOpenSearch={onOpenSearch}
|
||||||
|
loadMore={loadMore}
|
||||||
|
hasMore={!isReachingEnd}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -38,3 +38,20 @@ export type SearchFilter = {
|
|||||||
search_type?: SearchSource[];
|
search_type?: SearchSource[];
|
||||||
event_id?: string;
|
event_id?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SearchQueryParams = {
|
||||||
|
cameras?: string[];
|
||||||
|
labels?: string[];
|
||||||
|
sub_labels?: string[];
|
||||||
|
zones?: string[];
|
||||||
|
before?: string;
|
||||||
|
after?: string;
|
||||||
|
search_type?: string;
|
||||||
|
limit?: number;
|
||||||
|
in_progress?: number;
|
||||||
|
include_thumbnails?: number;
|
||||||
|
query?: string;
|
||||||
|
page?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SearchQuery = [string, SearchQueryParams] | null;
|
@ -94,7 +94,7 @@ function ThumbnailRow({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg bg-background_alt p-2 md:p-4">
|
<div className="rounded-lg bg-background_alt p-2 md:px-4">
|
||||||
<div className="text-lg capitalize">
|
<div className="text-lg capitalize">
|
||||||
{objectType.replaceAll("_", " ")}
|
{objectType.replaceAll("_", " ")}
|
||||||
{searchResults && (
|
{searchResults && (
|
||||||
|
@ -33,6 +33,8 @@ type SearchViewProps = {
|
|||||||
setSimilaritySearch: (search: SearchResult) => void;
|
setSimilaritySearch: (search: SearchResult) => void;
|
||||||
onUpdateFilter: (filter: SearchFilter) => void;
|
onUpdateFilter: (filter: SearchFilter) => void;
|
||||||
onOpenSearch: (item: SearchResult) => void;
|
onOpenSearch: (item: SearchResult) => void;
|
||||||
|
loadMore: () => void;
|
||||||
|
hasMore: boolean;
|
||||||
};
|
};
|
||||||
export default function SearchView({
|
export default function SearchView({
|
||||||
search,
|
search,
|
||||||
@ -43,6 +45,8 @@ export default function SearchView({
|
|||||||
setSearch,
|
setSearch,
|
||||||
setSimilaritySearch,
|
setSimilaritySearch,
|
||||||
onUpdateFilter,
|
onUpdateFilter,
|
||||||
|
loadMore,
|
||||||
|
hasMore,
|
||||||
}: SearchViewProps) {
|
}: SearchViewProps) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config", {
|
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
@ -143,7 +147,37 @@ export default function SearchView({
|
|||||||
scrollMode: "if-needed",
|
scrollMode: "if-needed",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [selectedIndex, uniqueResults]);
|
// we only want to scroll when the index changes
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedIndex]);
|
||||||
|
|
||||||
|
// observer for loading more
|
||||||
|
|
||||||
|
const observerTarget = useRef<HTMLDivElement>(null);
|
||||||
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (entries[0].isIntersecting && hasMore && !isLoading) {
|
||||||
|
loadMore();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 1.0 },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (observerTarget.current) {
|
||||||
|
observer.observe(observerTarget.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
observerRef.current = observer;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (observerRef.current) {
|
||||||
|
observerRef.current.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [hasMore, isLoading, loadMore]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col pt-2 md:py-2">
|
<div className="flex size-full flex-col pt-2 md:py-2">
|
||||||
@ -199,20 +233,23 @@ export default function SearchView({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="no-scrollbar flex flex-1 flex-wrap content-start gap-2 overflow-y-auto md:gap-4">
|
<div className="no-scrollbar flex flex-1 flex-wrap content-start gap-2 overflow-y-auto">
|
||||||
{searchTerm.length > 0 && searchResults?.length == 0 && (
|
{uniqueResults?.length == 0 && !isLoading && (
|
||||||
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
|
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
|
||||||
<LuSearchX className="size-16" />
|
<LuSearchX className="size-16" />
|
||||||
No Tracked Objects Found
|
No Tracked Objects Found
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoading && (
|
{uniqueResults?.length == 0 &&
|
||||||
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
isLoading &&
|
||||||
)}
|
searchFilter &&
|
||||||
|
Object.keys(searchFilter).length !== 0 && (
|
||||||
|
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
|
)}
|
||||||
|
|
||||||
{uniqueResults && (
|
{uniqueResults && (
|
||||||
<div className="mt-2 grid w-full gap-2 px-1 sm:grid-cols-2 md:mx-2 md:grid-cols-4 md:gap-4 3xl:grid-cols-6">
|
<div className="grid w-full gap-2 px-1 sm:grid-cols-2 md:mx-2 md:grid-cols-4 md:gap-4 3xl:grid-cols-6">
|
||||||
{uniqueResults &&
|
{uniqueResults &&
|
||||||
uniqueResults.map((value, index) => {
|
uniqueResults.map((value, index) => {
|
||||||
const selected = selectedIndex === index;
|
const selected = selectedIndex === index;
|
||||||
@ -273,12 +310,20 @@ export default function SearchView({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!uniqueResults && !isLoading && (
|
{uniqueResults && uniqueResults.length > 0 && (
|
||||||
<div className="scrollbar-container flex size-full flex-col overflow-y-auto">
|
<>
|
||||||
<ExploreView onSelectSearch={onSelectSearch} />
|
<div ref={observerTarget} className="h-10 w-full" />
|
||||||
</div>
|
<div className="flex h-12 w-full justify-center">
|
||||||
|
{hasMore && isLoading && <ActivityIndicator />}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{searchFilter && Object.keys(searchFilter).length === 0 && (
|
||||||
|
<div className="scrollbar-container flex size-full flex-col overflow-y-auto">
|
||||||
|
<ExploreView onSelectSearch={onSelectSearch} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user