Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions src/components/collections/filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import DatetimeSlider from "@/components/ui/datetime-slider";
import { useStore } from "@/store";
import { isCollectionInBbox, isCollectionInDatetimes } from "@/utils/stac";
import {
Checkbox,
CloseButton,
Input,
InputGroup,
Stack,
} from "@chakra-ui/react";
import { useEffect, useRef, useState } from "react";
import { LuFilter } from "react-icons/lu";
import type { StacCollection } from "stac-ts";

export default function Filter({
collections,
}: {
collections: StacCollection[];
}) {
const bbox = useStore((store) => store.bbox);
const datetimeFilter = useStore((store) => store.datetimeFilter);
const datetimeBounds = useStore((store) => store.datetimeBounds);
const setFilteredCollections = useStore(
(store) => store.setFilteredCollections
);
const inputRef = useRef<HTMLInputElement | null>(null);
const [includeGlobalCollections, setIncludeGlobalCollections] =
useState(true);
const [searchValue, setSearchValue] = useState("");

useEffect(() => {
setFilteredCollections(
collections?.filter(
(collection) =>
matchesFilter(collection, searchValue) &&
(!bbox ||
isCollectionInBbox(collection, bbox, includeGlobalCollections)) &&
(!datetimeFilter ||
isCollectionInDatetimes(
collection,
datetimeFilter.start,
datetimeFilter.end
))
) || null
);
}, [
collections,
setFilteredCollections,
searchValue,
bbox,
datetimeFilter,
includeGlobalCollections,
]);

return (
<Stack gap={4}>
<InputGroup
startElement={<LuFilter />}
endElement={
searchValue && (
<CloseButton
size={"xs"}
me="-2"
onClick={() => {
setSearchValue("");
inputRef.current?.focus();
}}
/>
)
}
>
<Input
placeholder="Filter collections by id or title"
ref={inputRef}
value={searchValue}
onChange={(e) => setSearchValue(e.currentTarget.value)}
/>
</InputGroup>
{datetimeBounds?.start && datetimeBounds?.end && (
<DatetimeSlider start={datetimeBounds.start} end={datetimeBounds.end} />
)}
<Checkbox.Root
onCheckedChange={(e) => setIncludeGlobalCollections(!!e.checked)}
checked={includeGlobalCollections}
size={"sm"}
>
<Checkbox.HiddenInput />
<Checkbox.Label>Include global collections</Checkbox.Label>
<Checkbox.Control />
</Checkbox.Root>
</Stack>
);
}

function matchesFilter(collection: StacCollection, filter: string) {
const lowerCaseFilter = filter.toLowerCase();
return (
collection.id.toLowerCase().includes(lowerCaseFilter) ||
collection.title?.toLowerCase().includes(lowerCaseFilter)
);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Section } from "@/components/section";
import { useStore } from "@/store";
import type { StacCollections } from "@/types/stac";
import { getLinkHref } from "@/utils/stac";
import {
ActionBar,
Button,
Expand All @@ -15,17 +17,8 @@ import {
type UseInfiniteQueryResult,
} from "@tanstack/react-query";
import { useEffect, useMemo, useState } from "react";
import {
LuFolderSymlink,
LuForward,
LuLoader,
LuPause,
LuPlay,
} from "react-icons/lu";
import { LuForward, LuLoader, LuPause, LuPlay } from "react-icons/lu";
import type { StacCollection } from "stac-ts";
import { useStore } from "../../store";
import type { StacCollections } from "../../types/stac";
import { getLinkHref } from "../../utils/stac";
import { ErrorAlert } from "../ui/error-alert";

export default function CollectionsHref({ href }: { href: string }) {
Expand Down Expand Up @@ -89,18 +82,29 @@ export default function CollectionsHref({ href }: { href: string }) {
);
else if (collections && result.hasNextPage)
return (
<PagedCollections
collections={collections}
numberMatched={numberMatched}
fetchAllCollections={fetchAllCollections}
setFetchAllCollections={setFetchAllCollections}
{...result}
/>
<>
<PaginationControls
collections={collections}
numberMatched={numberMatched}
fetchAllCollections={fetchAllCollections}
setFetchAllCollections={setFetchAllCollections}
{...result}
/>
<PagedCollectionsActionBar
collections={collections}
numberMatched={numberMatched}
fetchAllCollections={fetchAllCollections}
setFetchAllCollections={setFetchAllCollections}
fetchNextPage={result.fetchNextPage}
hasNextPage={result.hasNextPage}
isFetching={result.isFetching}
/>
</>
);
else if (result.isFetching) return <SkeletonText />;
}

function PagedCollections({
function PaginationControls({
collections,
numberMatched,
fetchAllCollections,
Expand All @@ -115,52 +119,39 @@ function PagedCollections({
setFetchAllCollections: (fetch: boolean) => void;
} & UseInfiniteQueryResult) {
return (
<>
<Section icon={<LuFolderSymlink />} title="Collection pagination">
<HStack width={"full"}>
{numberMatched ? (
<Progress.Root
width={"full"}
value={collections.length}
max={numberMatched}
striped={hasNextPage}
animated={fetchAllCollections || isFetching}
>
<Progress.Track>
<Progress.Range />
</Progress.Track>
</Progress.Root>
) : (
<Span width={"full"}>
{collections.length} collection
{collections.length === 1 ? "" : "s"} found
</Span>
)}
<ButtonGroup variant={"subtle"} size={"sm"}>
<IconButton
onClick={() => fetchNextPage()}
disabled={isFetching || fetchAllCollections}
>
{isFetching ? <LuLoader /> : <LuForward />}
</IconButton>
<IconButton
onClick={() => setFetchAllCollections(!fetchAllCollections)}
>
{fetchAllCollections && hasNextPage ? <LuPause /> : <LuPlay />}
</IconButton>
</ButtonGroup>
</HStack>
</Section>
<PagedCollectionsActionBar
collections={collections}
numberMatched={numberMatched}
fetchAllCollections={fetchAllCollections}
setFetchAllCollections={setFetchAllCollections}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
isFetching={isFetching}
/>
</>
<HStack width={"full"}>
{numberMatched ? (
<Progress.Root
width={"full"}
value={collections.length}
max={numberMatched}
striped={hasNextPage}
animated={fetchAllCollections || isFetching}
>
<Progress.Track>
<Progress.Range />
</Progress.Track>
</Progress.Root>
) : (
<Span width={"full"}>
{collections.length} collection
{collections.length === 1 ? "" : "s"} found
</Span>
)}
<ButtonGroup variant={"subtle"} size={"sm"}>
<IconButton
onClick={() => fetchNextPage()}
disabled={isFetching || fetchAllCollections}
>
{isFetching ? <LuLoader /> : <LuForward />}
</IconButton>
<IconButton
onClick={() => setFetchAllCollections(!fetchAllCollections)}
>
{fetchAllCollections && hasNextPage ? <LuPause /> : <LuPlay />}
</IconButton>
</ButtonGroup>
</HStack>
);
}

Expand Down
129 changes: 129 additions & 0 deletions src/components/collections/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Section } from "@/components/section";
import { useItems } from "@/hooks/store";
import { useStore } from "@/store";
import { getCollectionDatetimes } from "@/utils/stac";
import { Button, IconButton, Popover, Portal, Stack } from "@chakra-ui/react";
import { useEffect, useMemo } from "react";
import { LuEye, LuEyeClosed, LuFilter, LuFolderPlus } from "react-icons/lu";
import type { StacCollection } from "stac-ts";
import Filter from "./filter";
import CollectionsHref from "./href";
import CollectionList from "./list";
import Search from "./search";

export default function Collections({
href,
showSearch,
collections,
}: {
href: string | undefined;
showSearch: boolean;
collections: StacCollection[] | null;
}) {
const filteredCollections = useStore((store) => store.filteredCollections);
const setDatetimeBounds = useStore((store) => store.setDatetimeBounds);
const visualizeCollections = useStore((store) => store.visualizeCollections);
const setVisualizeCollections = useStore(
(store) => store.setVisualizeCollections
);
const items = useItems();
const hasItems = items && items.length > 0;

const { collectionsToShow, title } = useMemo(() => {
if (!collections) {
return { collectionsToShow: null, title: "Collections" };
}
return {
collectionsToShow: filteredCollections || collections,
title: filteredCollections
? `Collections (${filteredCollections.length}/${collections.length})`
: `Collections (${collections.length})`,
};
}, [filteredCollections, collections]);

useEffect(() => {
if (!collections) return;
const bounds = collections.reduce(
(acc, collection) => {
const { start, end } = getCollectionDatetimes(collection);
return {
start: start
? acc.start
? Math.min(acc.start, start.getTime())
: start.getTime()
: acc.start,
end: end
? acc.end
? Math.max(acc.end, end.getTime())
: end.getTime()
: acc.end,
};
},
{ start: null as number | null, end: null as number | null }
);
setDatetimeBounds({
start: bounds.start ? new Date(bounds.start) : null,
end: bounds.end ? new Date(bounds.end) : null,
});
}, [collections, setDatetimeBounds]);

const headerAction = hasItems ? (
<IconButton
size="2xs"
variant="ghost"
aria-label={
visualizeCollections
? "Hide collections on map"
: "Show collections on map"
}
onClick={(e) => {
e.stopPropagation();
setVisualizeCollections(!visualizeCollections);
}}
>
{visualizeCollections ? <LuEye /> : <LuEyeClosed />}
</IconButton>
) : undefined;

return (
<Section
icon={<LuFolderPlus />}
title={title}
headerAction={collections ? headerAction : undefined}
>
{(listOrCard) => (
<Stack gap={4}>
{showSearch && <Search />}
{href && <CollectionsHref href={href} />}
{collections && collectionsToShow && (
<>
{collections.length > 1 && (
<Popover.Root>
<Popover.Trigger asChild>
<Button variant="outline" size="sm">
<LuFilter /> Filter
</Button>
</Popover.Trigger>
<Portal>
<Popover.Positioner>
<Popover.Content>
<Popover.Arrow />
<Popover.Body>
<Filter collections={collections} />
</Popover.Body>
</Popover.Content>
</Popover.Positioner>
</Portal>
</Popover.Root>
)}
<CollectionList
collections={collectionsToShow}
listOrCard={listOrCard}
/>
</>
)}
</Stack>
)}
</Section>
);
}
Loading