From 303b7633f4c415c841533dc58d3b330885ac54ab Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Mon, 23 Feb 2026 05:56:54 -0500 Subject: [PATCH 1/3] refactor: move value to the top level --- src/components/{value/index.tsx => value.tsx} | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) rename src/components/{value/index.tsx => value.tsx} (83%) diff --git a/src/components/value/index.tsx b/src/components/value.tsx similarity index 83% rename from src/components/value/index.tsx rename to src/components/value.tsx index e164834..7e53253 100644 --- a/src/components/value/index.tsx +++ b/src/components/value.tsx @@ -10,23 +10,23 @@ import { import { Badge, Heading, HStack, Stack } from "@chakra-ui/react"; import { useEffect, useMemo } from "react"; import type { StacAsset } from "stac-ts"; -import Thumbnail from "../ui/thumbnail"; -import Assets from "./assets"; -import Breadcrumbs from "./breadcrumbs"; -import Buttons from "./buttons"; -import Catalogs from "./catalogs"; -import ChildLinks from "./child-links"; -import { CogHref, CogSources, PagedCogSources } from "./cogs"; -import CollectionSearch from "./collection-search"; -import Collections from "./collections"; -import CollectionsHref from "./collections-href"; -import Description from "./description"; -import ItemLinks from "./item-links"; -import Items from "./items"; -import Links from "./links"; -import Properties from "./properties"; -import RootHref from "./root-href"; -import StacGeoparquetHref from "./stac-geoparquet-href"; +import Thumbnail from "./ui/thumbnail"; +import Assets from "./value/assets"; +import Breadcrumbs from "./value/breadcrumbs"; +import Buttons from "./value/buttons"; +import Catalogs from "./value/catalogs"; +import ChildLinks from "./value/child-links"; +import { CogHref, CogSources, PagedCogSources } from "./value/cogs"; +import CollectionSearch from "./value/collection-search"; +import Collections from "./value/collections"; +import CollectionsHref from "./value/collections-href"; +import Description from "./value/description"; +import ItemLinks from "./value/item-links"; +import Items from "./value/items"; +import Links from "./value/links"; +import Properties from "./value/properties"; +import RootHref from "./value/root-href"; +import StacGeoparquetHref from "./value/stac-geoparquet-href"; export default function Value({ value }: { value: StacValue }) { const href = useStore((store) => store.href); From 2f26d60a7af797ddc76c4dd892dd1dfe7734bfdd Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Mon, 23 Feb 2026 06:25:55 -0500 Subject: [PATCH 2/3] fix: make a single panel for all collections --- src/components/collections/filter.tsx | 104 ++++++++ .../href.tsx} | 121 +++++----- src/components/collections/index.tsx | 135 +++++++++++ src/components/collections/list.tsx | 31 +++ .../search.tsx} | 13 +- src/components/value.tsx | 14 +- src/components/value/collections.tsx | 223 ------------------ 7 files changed, 336 insertions(+), 305 deletions(-) create mode 100644 src/components/collections/filter.tsx rename src/components/{value/collections-href.tsx => collections/href.tsx} (67%) create mode 100644 src/components/collections/index.tsx create mode 100644 src/components/collections/list.tsx rename src/components/{value/collection-search.tsx => collections/search.tsx} (81%) delete mode 100644 src/components/value/collections.tsx diff --git a/src/components/collections/filter.tsx b/src/components/collections/filter.tsx new file mode 100644 index 0000000..f6c25ae --- /dev/null +++ b/src/components/collections/filter.tsx @@ -0,0 +1,104 @@ +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(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 ( + + } + endElement={ + searchValue && ( + { + setSearchValue(""); + inputRef.current?.focus(); + }} + /> + ) + } + > + setSearchValue(e.currentTarget.value)} + /> + + {datetimeBounds?.start && datetimeBounds?.end && ( + + )} + setIncludeGlobalCollections(!!e.checked)} + checked={includeGlobalCollections} + size={"sm"} + > + + Include global collections + + + + ); +} + +function matchesFilter(collection: StacCollection, filter: string) { + const lowerCaseFilter = filter.toLowerCase(); + return ( + collection.id.toLowerCase().includes(lowerCaseFilter) || + collection.title?.toLowerCase().includes(lowerCaseFilter) + ); +} diff --git a/src/components/value/collections-href.tsx b/src/components/collections/href.tsx similarity index 67% rename from src/components/value/collections-href.tsx rename to src/components/collections/href.tsx index 7d066eb..d051926 100644 --- a/src/components/value/collections-href.tsx +++ b/src/components/collections/href.tsx @@ -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, @@ -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 }) { @@ -89,18 +82,29 @@ export default function CollectionsHref({ href }: { href: string }) { ); else if (collections && result.hasNextPage) return ( - + <> + + + ); else if (result.isFetching) return ; } -function PagedCollections({ +function PaginationControls({ collections, numberMatched, fetchAllCollections, @@ -115,52 +119,39 @@ function PagedCollections({ setFetchAllCollections: (fetch: boolean) => void; } & UseInfiniteQueryResult) { return ( - <> -
} title="Collection pagination"> - - {numberMatched ? ( - - - - - - ) : ( - - {collections.length} collection - {collections.length === 1 ? "" : "s"} found - - )} - - fetchNextPage()} - disabled={isFetching || fetchAllCollections} - > - {isFetching ? : } - - setFetchAllCollections(!fetchAllCollections)} - > - {fetchAllCollections && hasNextPage ? : } - - - -
- - + + {numberMatched ? ( + + + + + + ) : ( + + {collections.length} collection + {collections.length === 1 ? "" : "s"} found + + )} + + fetchNextPage()} + disabled={isFetching || fetchAllCollections} + > + {isFetching ? : } + + setFetchAllCollections(!fetchAllCollections)} + > + {fetchAllCollections && hasNextPage ? : } + + + ); } diff --git a/src/components/collections/index.tsx b/src/components/collections/index.tsx new file mode 100644 index 0000000..29fc5ba --- /dev/null +++ b/src/components/collections/index.tsx @@ -0,0 +1,135 @@ +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 ? ( + { + e.stopPropagation(); + setVisualizeCollections(!visualizeCollections); + }} + > + {visualizeCollections ? : } + + ) : undefined; + + return ( +
} + title={title} + headerAction={collections ? headerAction : undefined} + > + {(listOrCard) => ( + + {showSearch && } + {href && } + {collections && collectionsToShow && ( + <> + {collections.length > 1 && ( + + + + + + + + + + + + + + + + )} + + + )} + + )} +
+ ); +} diff --git a/src/components/collections/list.tsx b/src/components/collections/list.tsx new file mode 100644 index 0000000..4831844 --- /dev/null +++ b/src/components/collections/list.tsx @@ -0,0 +1,31 @@ +import CollectionCard from "@/components/cards/collection"; +import CollectionListItem from "@/components/list-items/collection"; +import type { ListOrCard } from "@/components/section"; +import { List, Stack } from "@chakra-ui/react"; +import type { StacCollection } from "stac-ts"; + +export default function CollectionList({ + collections, + listOrCard, +}: { + collections: StacCollection[]; + listOrCard: ListOrCard; +}) { + if (listOrCard === "list") { + return ( + + {collections.map((collection) => ( + + ))} + + ); + } + + return ( + + {collections.map((collection) => ( + + ))} + + ); +} diff --git a/src/components/value/collection-search.tsx b/src/components/collections/search.tsx similarity index 81% rename from src/components/value/collection-search.tsx rename to src/components/collections/search.tsx index 2889349..90eb714 100644 --- a/src/components/value/collection-search.tsx +++ b/src/components/collections/search.tsx @@ -1,4 +1,3 @@ -import { Section } from "@/components/section"; import { useStore } from "@/store"; import { Button, @@ -8,17 +7,9 @@ import { InputGroup, } from "@chakra-ui/react"; import { useRef, useState } from "react"; -import { LuFolderSearch, LuSearch } from "react-icons/lu"; +import { LuSearch } from "react-icons/lu"; -export default function CollectionSearch() { - return ( -
} title="Collection search"> - -
- ); -} - -function Search() { +export default function Search() { const [input, setInput] = useState(""); const inputRef = useRef(null); const setCollectionFreeTextSearch = useStore( diff --git a/src/components/value.tsx b/src/components/value.tsx index 7e53253..da820f0 100644 --- a/src/components/value.tsx +++ b/src/components/value.tsx @@ -17,9 +17,7 @@ import Buttons from "./value/buttons"; import Catalogs from "./value/catalogs"; import ChildLinks from "./value/child-links"; import { CogHref, CogSources, PagedCogSources } from "./value/cogs"; -import CollectionSearch from "./value/collection-search"; -import Collections from "./value/collections"; -import CollectionsHref from "./value/collections-href"; +import Collections from "./collections"; import Description from "./value/description"; import ItemLinks from "./value/item-links"; import Items from "./value/items"; @@ -92,9 +90,13 @@ export default function Value({ value }: { value: StacValue }) { - {conformsToFreeTextCollectionSearch(value) && } - {collectionsHref && } - {collections && } + {(collectionsHref || collections) && ( + + )} {catalogs && } {value.type === "Feature" && ( diff --git a/src/components/value/collections.tsx b/src/components/value/collections.tsx deleted file mode 100644 index e612712..0000000 --- a/src/components/value/collections.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import CollectionCard from "@/components/cards/collection"; -import CollectionListItem from "@/components/list-items/collection"; -import { Section } from "@/components/section"; -import DatetimeSlider from "@/components/ui/datetime-slider"; -import { useItems } from "@/hooks/store"; -import { useStore } from "@/store"; -import { - getCollectionDatetimes, - isCollectionInBbox, - isCollectionInDatetimes, -} from "@/utils/stac"; -import { - Button, - Checkbox, - CloseButton, - IconButton, - Input, - InputGroup, - List, - Popover, - Portal, - Stack, -} from "@chakra-ui/react"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { LuEye, LuEyeClosed, LuFilter, LuFolderPlus } from "react-icons/lu"; -import type { StacCollection } from "stac-ts"; - -export default function Collections({ - collections, -}: { - collections: StacCollection[]; -}) { - 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(() => { - return { - collectionsToShow: filteredCollections || collections, - title: filteredCollections - ? `Collections (${filteredCollections.length}/${collections.length})` - : `Collections (${collections.length})`, - }; - }, [filteredCollections, collections]); - - useEffect(() => { - 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 ? ( - { - e.stopPropagation(); - setVisualizeCollections(!visualizeCollections); - }} - > - {visualizeCollections ? : } - - ) : undefined; - - return ( -
} title={title} headerAction={headerAction}> - {(listOrCard) => { - return ( - - {collections.length > 1 && ( - - - - - - - - - - - - - - - - )} - {listOrCard === "list" ? ( - - {collectionsToShow.map((collection) => ( - - ))} - - ) : ( - - {collectionsToShow.map((collection) => ( - - ))} - - )} - - ); - }} -
- ); -} - -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(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 ( - - } - endElement={ - searchValue && ( - { - setSearchValue(""); - inputRef.current?.focus(); - }} - /> - ) - } - > - setSearchValue(e.currentTarget.value)} - /> - - {datetimeBounds?.start && datetimeBounds?.end && ( - - )} - setIncludeGlobalCollections(!!e.checked)} - checked={includeGlobalCollections} - size={"sm"} - > - - Include global collections - - - - ); -} - -function matchesFilter(collection: StacCollection, filter: string) { - const lowerCaseFilter = filter.toLowerCase(); - return ( - collection.id.toLowerCase().includes(lowerCaseFilter) || - collection.title?.toLowerCase().includes(lowerCaseFilter) - ); -} From 7a9aa53356bb9e76a65c644cf7cb0f8685bf64b8 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Mon, 23 Feb 2026 06:30:08 -0500 Subject: [PATCH 3/3] fix: formatting --- src/components/collections/filter.tsx | 5 +---- src/components/collections/index.tsx | 8 +------- src/components/value.tsx | 2 +- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/components/collections/filter.tsx b/src/components/collections/filter.tsx index f6c25ae..8de3a6e 100644 --- a/src/components/collections/filter.tsx +++ b/src/components/collections/filter.tsx @@ -1,9 +1,6 @@ import DatetimeSlider from "@/components/ui/datetime-slider"; import { useStore } from "@/store"; -import { - isCollectionInBbox, - isCollectionInDatetimes, -} from "@/utils/stac"; +import { isCollectionInBbox, isCollectionInDatetimes } from "@/utils/stac"; import { Checkbox, CloseButton, diff --git a/src/components/collections/index.tsx b/src/components/collections/index.tsx index 29fc5ba..d6902a6 100644 --- a/src/components/collections/index.tsx +++ b/src/components/collections/index.tsx @@ -2,13 +2,7 @@ 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 { 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"; diff --git a/src/components/value.tsx b/src/components/value.tsx index da820f0..1c23f66 100644 --- a/src/components/value.tsx +++ b/src/components/value.tsx @@ -10,6 +10,7 @@ import { import { Badge, Heading, HStack, Stack } from "@chakra-ui/react"; import { useEffect, useMemo } from "react"; import type { StacAsset } from "stac-ts"; +import Collections from "./collections"; import Thumbnail from "./ui/thumbnail"; import Assets from "./value/assets"; import Breadcrumbs from "./value/breadcrumbs"; @@ -17,7 +18,6 @@ import Buttons from "./value/buttons"; import Catalogs from "./value/catalogs"; import ChildLinks from "./value/child-links"; import { CogHref, CogSources, PagedCogSources } from "./value/cogs"; -import Collections from "./collections"; import Description from "./value/description"; import ItemLinks from "./value/item-links"; import Items from "./value/items";