diff --git a/src/app/App.js b/src/app/App.js index b117efe..72c802f 100644 --- a/src/app/App.js +++ b/src/app/App.js @@ -1,7 +1,7 @@ import React from "react"; import { ApolloProvider } from "@apollo/client"; import { Auth0Provider } from "@auth0/auth0-react"; -import { CSSReset, ChakraProvider, extendTheme } from "@chakra-ui/react"; +import { CSSReset, ChakraProvider, extendTheme, Box } from "@chakra-ui/react"; import { mode } from "@chakra-ui/theme-tools"; import { BrowserRouter as Router, @@ -45,6 +45,17 @@ const WardrobePage = loadable(() => import("./WardrobePage"), { fallback: , }); +// ItemPage and ItemSearchPage need to share a search toolbar, so here it is! +// It'll load in dynamically like the page elements, with a hacky fallback to +// take up 40px of height until it loads. +// +// There very well be a better way to encapsulate this! It's not *great* to +// have this here. I just don't wanna over abstract it just yet 😅 +const ItemSearchPageToolbar = loadable( + () => import("./components/ItemSearchPageToolbar"), + { fallback: } +); + const theme = extendTheme({ styles: { global: (props) => ({ @@ -116,6 +127,7 @@ function App() { + @@ -131,6 +143,7 @@ function App() { + diff --git a/src/app/ItemPage.js b/src/app/ItemPage.js index 0cb0006..dc69b91 100644 --- a/src/app/ItemPage.js +++ b/src/app/ItemPage.js @@ -31,7 +31,7 @@ import { useQuery, useMutation } from "@apollo/client"; import { Link, useParams } from "react-router-dom"; import ItemPageLayout, { SubtleSkeleton } from "./ItemPageLayout"; -import { Delay, logAndCapture, usePageTitle } from "./util"; +import { Delay, logAndCapture, useLocalStorage, usePageTitle } from "./util"; import HTML5Badge, { layerUsesHTML5 } from "./components/HTML5Badge"; import { itemAppearanceFragment, @@ -44,7 +44,6 @@ import SpeciesColorPicker, { getClosestPose, } from "./components/SpeciesColorPicker"; import useCurrentUser from "./components/useCurrentUser"; -import { useLocalStorage } from "./util"; import SpeciesFacesPicker, { colorIsBasic, } from "./ItemPage/SpeciesFacesPicker"; diff --git a/src/app/ItemSearchPage.js b/src/app/ItemSearchPage.js index 38ca1f9..8378067 100644 --- a/src/app/ItemSearchPage.js +++ b/src/app/ItemSearchPage.js @@ -1,98 +1,21 @@ -import React from "react"; import { Box, Wrap, WrapItem } from "@chakra-ui/react"; import gql from "graphql-tag"; import { useQuery } from "@apollo/client"; -import { useHistory, useLocation, useParams } from "react-router-dom"; -import SearchToolbar, { +import { emptySearchQuery, searchQueryIsEmpty, } from "./WardrobePage/SearchToolbar"; import SquareItemCard, { SquareItemCardSkeleton, } from "./components/SquareItemCard"; -import { Delay, MajorErrorMessage, useCommonStyles, useDebounce } from "./util"; +import { Delay, MajorErrorMessage, useDebounce } from "./util"; import PaginationToolbar from "./components/PaginationToolbar"; +import { useSearchQueryInUrl } from "./components/ItemSearchPageToolbar"; function ItemSearchPage() { - const [query, offset, setQuery] = useSearchQueryInUrl(); - const { brightBackground } = useCommonStyles(); + const { query: latestQuery, offset } = useSearchQueryInUrl(); - return ( - - - - - - ); -} - -/** - * useSearchQueryInUrl provides an API like useState, but stores the search - * query in the URL! It also parses out the offset for us. - */ -function useSearchQueryInUrl() { - const history = useHistory(); - - const { query: value } = useParams(); - const { search } = useLocation(); - const searchParams = new URLSearchParams(search); - - const query = { - value: decodeURIComponent(value || ""), - filterToZoneLabel: searchParams.get("zone") || null, - filterToItemKind: searchParams.get("kind") || null, - filterToCurrentUserOwnsOrWants: searchParams.get("user") || null, - }; - - const offset = parseInt(searchParams.get("offset")) || 0; - - const setQuery = React.useCallback( - (newQuery) => { - let url = `/items/search`; - - if (newQuery.value) { - url += "/" + encodeURIComponent(newQuery.value); - } - - const newParams = new URLSearchParams(); - if (newQuery.filterToItemKind) { - newParams.append("kind", newQuery.filterToItemKind); - } - if (newQuery.filterToZoneLabel) { - newParams.append("zone", newQuery.filterToZoneLabel); - } - if (newQuery.filterToCurrentUserOwnsOrWants) { - newParams.append("user", newQuery.filterToCurrentUserOwnsOrWants); - } - - // NOTE: We omit `offset`, because changing the query should reset us - // back to the first page! - - const search = newParams.toString(); - if (search) { - url += "?" + search; - } - - history.replace(url); - }, - [history] - ); - - // NOTE: We don't provide a `setOffset`, because that's handled via - // pagination links. - - return [query, offset, setQuery]; -} - -function ItemSearchPageResults({ query: latestQuery, offset }) { // NOTE: Some of this is copied from SearchPanel... but all of this is messy // enough that I'm not comfy code-sharing yet, esp since I feel like // SearchPanel pagination is a bit of a mess and will need refactoring. diff --git a/src/app/components/ItemSearchPageToolbar.js b/src/app/components/ItemSearchPageToolbar.js new file mode 100644 index 0000000..f3ea319 --- /dev/null +++ b/src/app/components/ItemSearchPageToolbar.js @@ -0,0 +1,106 @@ +import React from "react"; +import { useHistory, useLocation, useParams } from "react-router-dom"; +import { useCommonStyles } from "../util"; +import SearchToolbar from "../WardrobePage/SearchToolbar"; + +function ItemSearchPageToolbar({ ...props }) { + const { query, setQuery } = useSearchQueryInUrl(); + const { brightBackground } = useCommonStyles(); + + return ( + + ); +} + +/** + * useSearchQueryInUrl provides an API like useState, but stores the search + * query in the URL! It also parses out the offset for us. + */ +export function useSearchQueryInUrl() { + const history = useHistory(); + + const { query: value } = useParams(); + const { pathname, search } = useLocation(); + + // Parse the query from the location. (We memoize this because we use it as a + // dependency in the query-saving hook below.) + const parsedQuery = React.useMemo(() => { + const searchParams = new URLSearchParams(search); + return { + value: decodeURIComponent(value || ""), + filterToZoneLabel: searchParams.get("zone") || null, + filterToItemKind: searchParams.get("kind") || null, + filterToCurrentUserOwnsOrWants: searchParams.get("user") || null, + }; + }, [search, value]); + + const offset = parseInt(new URLSearchParams(search).get("offset")) || 0; + + // While on the search page, save the most recent parsed query in state. + const isSearchPage = pathname.startsWith("/items/search"); + const [savedQuery, setSavedQuery] = React.useState(parsedQuery); + React.useEffect(() => { + if (isSearchPage) { + setSavedQuery(parsedQuery); + } + }, [isSearchPage, parsedQuery]); + + // Then, while not on the search page, use the saved query from state, + // instead of the (presumably empty) parsed query from the URL. + const query = isSearchPage ? parsedQuery : savedQuery; + + const setQuery = React.useCallback( + (newQuery) => { + let url = `/items/search`; + + if (newQuery.value) { + url += "/" + encodeURIComponent(newQuery.value); + } + + const newParams = new URLSearchParams(); + if (newQuery.filterToItemKind) { + newParams.append("kind", newQuery.filterToItemKind); + } + if (newQuery.filterToZoneLabel) { + newParams.append("zone", newQuery.filterToZoneLabel); + } + if (newQuery.filterToCurrentUserOwnsOrWants) { + newParams.append("user", newQuery.filterToCurrentUserOwnsOrWants); + } + + // NOTE: We omit `offset`, because changing the query should reset us + // back to the first page! + const search = newParams.toString(); + if (search) { + url += "?" + search; + } + + // TODO: Tbh would be even nicer for this to be a like... timed thing? + // We use replace to avoid spamming the history too much, but sometimes + // the user's query meaningfully *does* change without intermediate + // navigation, like if they see the results and decide it's the wrong + // thing. + if (isSearchPage) { + history.replace(url); + } else { + // When you use the search toolbar from the item page, treat it as a + // full navigation! + history.push(url); + } + }, + [history, isSearchPage] + ); + + // NOTE: We don't provide a `setOffset`, because that's handled via + // pagination links. + return { query, offset, setQuery }; +} + +export default ItemSearchPageToolbar;