From 8e5e0747ab72d9524bc29d6e41a5c93b5dc49fae Mon Sep 17 00:00:00 2001 From: hta218 Date: Mon, 16 Dec 2024 15:43:40 +0700 Subject: [PATCH 1/8] Update 404 page layout --- app/components/root/not-found.tsx | 61 +++++++++++++++------- app/root.tsx | 4 +- app/routes/($locale).api.featured-items.ts | 7 +-- 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/app/components/root/not-found.tsx b/app/components/root/not-found.tsx index fd5befb1..b1577985 100644 --- a/app/components/root/not-found.tsx +++ b/app/components/root/not-found.tsx @@ -1,40 +1,65 @@ import { useFetcher } from "@remix-run/react"; import { useEffect } from "react"; +import { BreadCrumb } from "~/components/breadcrumb"; import Link from "~/components/link"; +import { ProductCard } from "~/components/product/product-card"; +import { Section } from "~/components/section"; +import { Swimlane } from "~/components/swimlane"; import { usePrefixPathWithLocale } from "~/lib/utils"; -import { ProductSwimlane } from "~/modules/product-swimlane"; import type { FeaturedData } from "~/routes/($locale).api.featured-items"; -import { PageHeader, Text } from "../../modules/text"; export function NotFound({ type = "page" }: { type?: string }) { return ( - <> - - - We couldn’t find the {type} you’re looking for. Try checking the URL - or heading back to the home page. - - - Take me to the home page - - +
+
+ +

+ We’ve lost this {type} +

+

+ We couldn’t find the {type} you’re looking for. It may have been + removed, had its name changed, or is temporarily unavailable. +

+
+ + Shop our products + + Or + + Take me to the home page + +
+
- +
); } export function FeaturedItemsSection() { let { load, data } = useFetcher(); - let path = usePrefixPathWithLocale("/api/featured-items"); + let api = usePrefixPathWithLocale("/api/featured-items"); // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { - load(path); - }, [path]); + load(api); + }, [api]); if (!data) return null; - let { featuredProducts } = data; + let { featuredCollections, featuredProducts } = data; - return ; + return ( +
+
Featured products
+ + {featuredProducts.nodes.map((product) => ( + + ))} + +
+ ); } diff --git a/app/root.tsx b/app/root.tsx index 62781542..2475d175 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -182,7 +182,9 @@ export function ErrorBoundary({ error }: { error: Error }) { let pageType = "page"; if (isRouteError) { - if (routeError.status === 404) pageType = routeError.data || pageType; + if (routeError.status === 404) { + pageType = routeError.data || pageType; + } } return ( diff --git a/app/routes/($locale).api.featured-items.ts b/app/routes/($locale).api.featured-items.ts index 8bd0ac6e..0f612f8d 100644 --- a/app/routes/($locale).api.featured-items.ts +++ b/app/routes/($locale).api.featured-items.ts @@ -1,4 +1,5 @@ import { type LoaderFunctionArgs, json } from "@shopify/remix-oxygen"; +import type { FeaturedItemsQuery } from "storefrontapi.generated"; import invariant from "tiny-invariant"; import { FEATURED_COLLECTION_FRAGMENT, @@ -11,9 +12,9 @@ export async function loader({ context: { storefront } }: LoaderFunctionArgs) { export async function getFeaturedData( storefront: LoaderFunctionArgs["context"]["storefront"], - variables: { pageBy?: number } = {} + variables: { pageBy?: number } = {}, ) { - const data = await storefront.query(FEATURED_ITEMS_QUERY, { + let data = await storefront.query(FEATURED_ITEMS_QUERY, { variables: { pageBy: 12, country: storefront.i18n.country, @@ -29,7 +30,7 @@ export async function getFeaturedData( export type FeaturedData = Awaited>; -export const FEATURED_ITEMS_QUERY = `#graphql +const FEATURED_ITEMS_QUERY = `#graphql query FeaturedItems( $country: CountryCode $language: LanguageCode From 9bb4b4f08352f30d6865daa7bae835d8b8b90cf7 Mon Sep 17 00:00:00 2001 From: hta218 Date: Mon, 16 Dec 2024 15:46:19 +0700 Subject: [PATCH 2/8] Remove unused product list section --- app/sections/product-list.tsx | 97 ----------------------------------- app/weaverse/components.ts | 2 - 2 files changed, 99 deletions(-) delete mode 100644 app/sections/product-list.tsx diff --git a/app/sections/product-list.tsx b/app/sections/product-list.tsx deleted file mode 100644 index f1a18f41..00000000 --- a/app/sections/product-list.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { getPaginationVariables } from "@shopify/hydrogen"; -import type { - ComponentLoaderArgs, - HydrogenComponentProps, - HydrogenComponentSchema, -} from "@weaverse/hydrogen"; -import { forwardRef } from "react"; -import { COLLECTION_QUERY } from "~/data/queries"; -import { getSortValuesFromParam } from "~/lib/collections"; -import { PAGINATION_SIZE } from "~/lib/const"; -import type { SortParam } from "~/lib/filter"; -import { ProductSwimlane } from "~/modules/product-swimlane"; - -interface ProductListProps - extends HydrogenComponentProps>> { - heading: string; - productsCount: number; -} - -let ProductList = forwardRef((props, ref) => { - let { loaderData, heading, productsCount, ...rest } = props; - let products = loaderData?.collection?.products; - return ( -
- {products?.nodes?.length ? ( - - ) : null} -
- ); -}); - -export default ProductList; - -export let loader = async ({ weaverse, data }: ComponentLoaderArgs) => { - let { language, country } = weaverse.storefront.i18n; - let collectionHandle = data?.collection?.handle; - - const searchParams = new URL(weaverse.request.url).searchParams; - - const { sortKey, reverse } = getSortValuesFromParam( - searchParams.get("sort") as SortParam, - ); - const paginationVariables = getPaginationVariables(weaverse.request, { - pageBy: PAGINATION_SIZE, - }); - return await weaverse.storefront.query(COLLECTION_QUERY, { - variables: { - ...paginationVariables, - handle: collectionHandle, - country, - language, - sortKey, - reverse, - filters: [], - }, - }); -}; - -export let schema: HydrogenComponentSchema = { - type: "product-list", - title: "Product List", - limit: 1, - inspector: [ - { - group: "Product List", - inputs: [ - { - type: "collection", - name: "collection", - label: "Collection", - }, - { - type: "text", - name: "heading", - label: "Heading", - defaultValue: "Product List", - placeholder: "Product List", - }, - { - type: "range", - name: "productsCount", - label: "Number of products", - defaultValue: 4, - configs: { - min: 1, - max: 12, - step: 1, - }, - }, - ], - }, - ], -}; diff --git a/app/weaverse/components.ts b/app/weaverse/components.ts index 19ca8134..a6513765 100644 --- a/app/weaverse/components.ts +++ b/app/weaverse/components.ts @@ -39,7 +39,6 @@ import * as OurTeam from "~/sections/our-team"; import * as OurTeamMembers from "~/sections/our-team/team-members"; import * as Page from "~/sections/page"; import * as ProductInformation from "~/sections/product-information"; -import * as ProductList from "~/sections/product-list"; import * as PromotionGrid from "~/sections/promotion-grid"; import * as PromotionGridButtons from "~/sections/promotion-grid/buttons"; import * as PromotionGridItem from "~/sections/promotion-grid/item"; @@ -112,6 +111,5 @@ export let components: HydrogenComponent[] = [ OurTeamMembers, SlideShow, SlideShowSlide, - ProductList, Spacer, ]; From 690ea3b23abcdd2d63ea7cdf202f90dda70c59d7 Mon Sep 17 00:00:00 2001 From: hta218 Date: Mon, 16 Dec 2024 15:49:13 +0700 Subject: [PATCH 3/8] Update account page, remove unused product swimlane component --- app/modules/product-swimlane.tsx | 33 ------------------------- app/routes/($locale).account.tsx | 42 ++++++++++++++++++-------------- 2 files changed, 24 insertions(+), 51 deletions(-) delete mode 100644 app/modules/product-swimlane.tsx diff --git a/app/modules/product-swimlane.tsx b/app/modules/product-swimlane.tsx deleted file mode 100644 index 3d43d61a..00000000 --- a/app/modules/product-swimlane.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Section } from "~/modules/text"; -import { ProductCard } from "~/components/product/product-card"; - -const mockProducts = { - nodes: new Array(12).fill(""), -}; - -type ProductSwimlaneProps = { - title?: string; - count?: number; - products?: any; -}; - -export function ProductSwimlane({ - title = "Featured Products", - products = mockProducts, - count = 12, - ...props -}: ProductSwimlaneProps) { - return ( -
-
- {products.nodes.slice(0, count).map((product) => ( - - ))} -
-
- ); -} diff --git a/app/routes/($locale).account.tsx b/app/routes/($locale).account.tsx index 11ba2c6e..f4a4f548 100644 --- a/app/routes/($locale).account.tsx +++ b/app/routes/($locale).account.tsx @@ -14,11 +14,12 @@ import { Suspense } from "react"; import { AccountDetails } from "~/components/account/account-details"; import { AccountAddressBook } from "~/components/account/address-book"; import { AccountOrderHistory } from "~/components/account/orders"; +import { ProductCard } from "~/components/product/product-card"; +import { Swimlane } from "~/components/swimlane"; import { CACHE_NONE, routeHeaders } from "~/data/cache"; import { CUSTOMER_DETAILS_QUERY } from "~/graphql/customer-account/customer-details-query"; import { usePrefixPathWithLocale } from "~/lib/utils"; import { Modal } from "~/modules/modal"; -import { ProductSwimlane } from "~/modules/product-swimlane"; import { doLogout } from "./($locale).account_.logout"; import { type FeaturedData, @@ -27,9 +28,9 @@ import { export let headers = routeHeaders; -export async function loader({ request, context, params }: LoaderFunctionArgs) { +export async function loader({ context }: LoaderFunctionArgs) { let { data, errors } = await context.customerAccount.query( - CUSTOMER_DETAILS_QUERY + CUSTOMER_DETAILS_QUERY, ); /** @@ -40,20 +41,12 @@ export async function loader({ request, context, params }: LoaderFunctionArgs) { } let customer = data?.customer; - let heading = customer ? "My Account" : "Account Details"; + let featuredData = getFeaturedData(context.storefront); return defer( - { - customer, - heading, - featuredDataPromise: getFeaturedData(context.storefront), - }, - { - headers: { - "Cache-Control": CACHE_NONE, - }, - } + { customer, heading, featuredData }, + { headers: { "Cache-Control": CACHE_NONE } }, ); } @@ -87,11 +80,11 @@ export default function Authenticated() { interface AccountType { customer: CustomerDetailsFragment; - featuredDataPromise: Promise; + featuredData: Promise; heading: string; } -function Account({ customer, heading, featuredDataPromise }: AccountType) { +function Account({ customer, heading, featuredData }: AccountType) { let orders = flattenConnection(customer.orders); let addresses = flattenConnection(customer.addresses); @@ -115,10 +108,23 @@ function Account({ customer, heading, featuredDataPromise }: AccountType) { {!orders.length && ( - {(data) => } + {({ featuredProducts }) => ( +
+
Featured products
+ + {featuredProducts.nodes.map((product) => ( + + ))} + +
+ )}
)} From d096aa20980aed1237362265961089f74bfae418 Mon Sep 17 00:00:00 2001 From: hta218 Date: Mon, 16 Dec 2024 16:06:34 +0700 Subject: [PATCH 4/8] Update predictive search form --- .../layout/predictive-search/index.tsx | 64 ++++++++++--------- .../layout/predictive-search/search-form.tsx | 33 +++++++--- app/modules/grid.tsx | 43 ------------- app/routes/($locale).search.tsx | 2 +- app/types/predictive-search.ts | 14 ---- 5 files changed, 58 insertions(+), 98 deletions(-) delete mode 100644 app/modules/grid.tsx diff --git a/app/components/layout/predictive-search/index.tsx b/app/components/layout/predictive-search/index.tsx index 443f3a91..7be87a78 100644 --- a/app/components/layout/predictive-search/index.tsx +++ b/app/components/layout/predictive-search/index.tsx @@ -1,8 +1,7 @@ -import { MagnifyingGlass } from "@phosphor-icons/react"; +import { MagnifyingGlass, X } from "@phosphor-icons/react"; import * as Dialog from "@radix-ui/react-dialog"; import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; import { cn } from "~/lib/cn"; -import { Input } from "~/modules/input"; import { PredictiveSearchResults } from "./predictive-search-results"; import { PredictiveSearchForm } from "./search-form"; @@ -36,37 +35,40 @@ export function PredictiveSearchButton() { Predictive search - +
+ + {({ fetchResults, inputRef }) => ( +
+ + fetchResults(e.target.value)} + onFocus={(e) => fetchResults(e.target.value)} + placeholder="Enter a keyword" + ref={inputRef} + autoComplete="off" + className="focus-visible:outline-none w-full h-full py-4" + /> + +
+ )} +
+ +
); } - -function PredictiveSearch() { - return ( -
- - {({ fetchResults, inputRef }) => ( -
- - } - autoFocus={true} - /> -
- )} -
- -
- ); -} diff --git a/app/components/layout/predictive-search/search-form.tsx b/app/components/layout/predictive-search/search-form.tsx index 13574dfc..da0a3abb 100644 --- a/app/components/layout/predictive-search/search-form.tsx +++ b/app/components/layout/predictive-search/search-form.tsx @@ -1,9 +1,25 @@ -import { useFetcher, useParams } from "@remix-run/react"; -import { useEffect, useRef } from "react"; -import type { - NormalizedPredictiveSearchResults, - SearchFromProps, -} from "~/types/predictive-search"; +import { type FormProps, useFetcher, useParams } from "@remix-run/react"; +import { + type MutableRefObject, + type ReactNode, + useEffect, + useRef, +} from "react"; +import type { NormalizedPredictiveSearchResults } from "~/types/predictive-search"; + +type ChildrenRenderProps = { + fetchResults: (event: string) => void; + fetcher: ReturnType>; + inputRef: MutableRefObject; +}; + +type SearchFromProps = { + action?: FormProps["action"]; + method?: FormProps["method"]; + className?: string; + children: (passedProps: ChildrenRenderProps) => ReactNode; + [key: string]: unknown; +}; /** * Search form component that posts search requests to the `/search` route @@ -19,14 +35,13 @@ export function PredictiveSearchForm({ let fetcher = useFetcher(); let inputRef = useRef(null); - function fetchResults(event: React.ChangeEvent) { + function fetchResults(searchTerm: string) { let searchAction = action ?? "/api/predictive-search"; let localizedAction = params.locale ? `/${params.locale}${searchAction}` : searchAction; - let newSearchTerm = event.target.value || ""; fetcher.submit( - { q: newSearchTerm, limit: "6" }, + { q: searchTerm, limit: "6" }, { method, action: localizedAction }, ); } diff --git a/app/modules/grid.tsx b/app/modules/grid.tsx deleted file mode 100644 index 710374ba..00000000 --- a/app/modules/grid.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import clsx from "clsx"; - -export function Grid({ - as: Component = "div", - className, - flow = "row", - gap = "default", - items = 4, - layout = "default", - numberInRow = 4, - ...props -}: { - as?: React.ElementType; - className?: string; - flow?: "row" | "col"; - gap?: "default" | "blog"; - items?: number; - layout?: "default" | "products" | "auto" | "blog"; - [key: string]: any; -}) { - const layouts = { - default: `grid-cols-1 ${items === 2 && "md:grid-cols-2"} ${ - items === 3 && "sm:grid-cols-3" - } ${items > 3 && "md:grid-cols-3"} ${items >= 4 && "lg:grid-cols-4"}`, - products: `${numberInRow === 4 ? "lg:grid-cols-4 grid-cols-2" : "grid-cols-1 lg:grid-cols-3"}`, - auto: "auto-cols-auto", - blog: "grid-cols-1 md:grid-cols-2", - }; - - const gaps = { - default: "grid gap-2 gap-y-6 md:gap-4 lg:gap-6", - blog: "grid gap-6", - }; - - const flows = { - row: "grid-flow-row", - col: "grid-flow-col", - }; - - const styles = clsx(flows[flow], gaps[gap], layouts[layout], className); - - return ; -} diff --git a/app/routes/($locale).search.tsx b/app/routes/($locale).search.tsx index 8090f3a2..3fecff42 100644 --- a/app/routes/($locale).search.tsx +++ b/app/routes/($locale).search.tsx @@ -117,7 +117,7 @@ export default function Search() {
& { item: NormalizedPredictiveSearchResultItem; }; - -type ChildrenRenderProps = { - fetchResults: (event: React.ChangeEvent) => void; - fetcher: ReturnType>; - inputRef: React.MutableRefObject; -}; - -export type SearchFromProps = { - action?: FormProps["action"]; - method?: FormProps["method"]; - className?: string; - children: (passedProps: ChildrenRenderProps) => React.ReactNode; - [key: string]: unknown; -}; From 1fea38b7582c285135e26566ebb2c324e8d637df Mon Sep 17 00:00:00 2001 From: hta218 Date: Mon, 16 Dec 2024 16:15:58 +0700 Subject: [PATCH 5/8] Update predictive search result item --- .../predictive-search-result.tsx | 134 ++++++++++++------ .../layout/predictive-search/result-item.tsx | 75 ---------- app/types/predictive-search.ts | 15 -- 3 files changed, 88 insertions(+), 136 deletions(-) delete mode 100644 app/components/layout/predictive-search/result-item.tsx diff --git a/app/components/layout/predictive-search/predictive-search-result.tsx b/app/components/layout/predictive-search/predictive-search-result.tsx index ec574dd4..4720bef1 100644 --- a/app/components/layout/predictive-search/predictive-search-result.tsx +++ b/app/components/layout/predictive-search/predictive-search-result.tsx @@ -1,40 +1,35 @@ -import { Link } from "@remix-run/react"; import clsx from "clsx"; import type { NormalizedPredictiveSearchResultItem, NormalizedPredictiveSearchResults, - SearchResultTypeProps, } from "~/types/predictive-search"; -import { SearchResultItem } from "./result-item"; +import { Image, Money } from "@shopify/hydrogen"; +import type { MoneyV2 } from "@shopify/hydrogen/storefront-api-types"; +import { Link } from "~/components/link"; +import { CompareAtPrice } from "~/components/compare-at-price"; +import { getImageAspectRatio, isDiscounted } from "~/lib/utils"; + +type SearchResultTypeProps = { + goToSearchResult: (event: React.MouseEvent) => void; + items?: NormalizedPredictiveSearchResultItem[]; + type: NormalizedPredictiveSearchResults[number]["type"]; +}; export function PredictiveSearchResult({ goToSearchResult, items, - searchTerm, type, }: SearchResultTypeProps) { let isSuggestions = type === "queries"; - let categoryUrl = `/search?q=${ - searchTerm.current - }&type=${pluralToSingularSearchType(type)}`; return ( -
- +
+
{isSuggestions ? "Suggestions" : type} - - {items?.length && ( +
+ {items?.length ? (
    ))}
+ ) : ( +
+ No {isSuggestions ? "suggestions" : type} available. +
)}
); } -/** - * Converts a plural search type to a singular search type - * - * @example - * ```js - * pluralToSingularSearchType('articles'); // => 'ARTICLE' - * pluralToSingularSearchType(['articles', 'products']); // => 'ARTICLE,PRODUCT' - * ``` - */ -function pluralToSingularSearchType( - type: - | NormalizedPredictiveSearchResults[number]["type"] - | Array, -) { - let plural = { - articles: "ARTICLE", - collections: "COLLECTION", - pages: "PAGE", - products: "PRODUCT", - queries: "QUERY", - }; +type SearchResultItemProps = Pick & { + item: NormalizedPredictiveSearchResultItem; +}; - if (typeof type === "string") { - return plural[type]; - } - - return type.map((t) => plural[t]).join(","); +function SearchResultItem({ + goToSearchResult, + item: { + id, + __typename, + image, + compareAtPrice, + price, + title, + url, + vendor, + styledTitle, + }, +}: SearchResultItemProps) { + return ( +
  • + + {__typename === "Product" && ( +
    + {image?.url && ( + {image.altText + )} +
    + )} +
    + {vendor && ( +
    By {vendor}
    + )} + {styledTitle ? ( +
    + ) : ( +
    + {title} +
    + )} + {price && ( +
    + + {isDiscounted(price as MoneyV2, compareAtPrice as MoneyV2) && ( + + )} +
    + )} +
    + +
  • + ); } diff --git a/app/components/layout/predictive-search/result-item.tsx b/app/components/layout/predictive-search/result-item.tsx deleted file mode 100644 index 6f9568ea..00000000 --- a/app/components/layout/predictive-search/result-item.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Image, Money } from "@shopify/hydrogen"; -import type { MoneyV2 } from "@shopify/hydrogen/storefront-api-types"; -import clsx from "clsx"; -import { Link } from "~/components/link"; -import { CompareAtPrice } from "~/components/compare-at-price"; -import { getImageAspectRatio, isDiscounted } from "~/lib/utils"; -import type { SearchResultItemProps } from "../../../types/predictive-search"; - -export function SearchResultItem({ - goToSearchResult, - item: { - id, - __typename, - image, - compareAtPrice, - price, - title, - url, - vendor, - styledTitle, - }, -}: SearchResultItemProps) { - return ( -
  • - - {__typename === "Product" && ( -
    - {image?.url && ( - {image.altText - )} -
    - )} -
    - {vendor && ( -
    By {vendor}
    - )} - {styledTitle ? ( -
    - ) : ( -
    - {title} -
    - )} - {price && ( -
    - - {isDiscounted(price as MoneyV2, compareAtPrice as MoneyV2) && ( - - )} -
    - )} -
    - -
  • - ); -} diff --git a/app/types/predictive-search.ts b/app/types/predictive-search.ts index f50cf7fb..8dbb3154 100644 --- a/app/types/predictive-search.ts +++ b/app/types/predictive-search.ts @@ -1,4 +1,3 @@ -import type { FormProps, useFetcher } from "@remix-run/react"; import type { PredictiveArticleFragment, PredictiveCollectionFragment, @@ -43,17 +42,3 @@ export type NormalizedPredictiveSearchResultItem = { vendor: string; url: string; }; - -export type SearchResultTypeProps = { - goToSearchResult: (event: React.MouseEvent) => void; - items?: NormalizedPredictiveSearchResultItem[]; - searchTerm: UseSearchReturn["searchTerm"]; - type: NormalizedPredictiveSearchResults[number]["type"]; -}; - -export type SearchResultItemProps = Pick< - SearchResultTypeProps, - "goToSearchResult" -> & { - item: NormalizedPredictiveSearchResultItem; -}; From e3542bc120c9e923b661ef54f8245322c19154e3 Mon Sep 17 00:00:00 2001 From: hta218 Date: Mon, 16 Dec 2024 16:34:20 +0700 Subject: [PATCH 6/8] Close predictive on go to a result item --- .../layout/predictive-search/index.tsx | 80 ++++++++++++++++++- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/app/components/layout/predictive-search/index.tsx b/app/components/layout/predictive-search/index.tsx index 7be87a78..a5ce0bb7 100644 --- a/app/components/layout/predictive-search/index.tsx +++ b/app/components/layout/predictive-search/index.tsx @@ -1,13 +1,25 @@ -import { MagnifyingGlass, X } from "@phosphor-icons/react"; +import { ArrowRight, MagnifyingGlass, X } from "@phosphor-icons/react"; import * as Dialog from "@radix-ui/react-dialog"; import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; +import { useLocation } from "@remix-run/react"; +import { type MutableRefObject, useEffect, useState } from "react"; +import Link from "~/components/link"; +import { usePredictiveSearch } from "~/hooks/use-predictive-search"; import { cn } from "~/lib/cn"; -import { PredictiveSearchResults } from "./predictive-search-results"; +import { PredictiveSearchResult } from "./predictive-search-result"; import { PredictiveSearchForm } from "./search-form"; export function PredictiveSearchButton() { + let [open, setOpen] = useState(false); + let location = useLocation(); + + // biome-ignore lint/correctness/useExhaustiveDependencies: close the dialog when the location changes, aka when the user navigates to a search result page + useEffect(() => { + setOpen(false); + }, [location]); + return ( - + ); } + +function PredictiveSearchResults() { + let { results, totalResults, searchTerm } = usePredictiveSearch(); + let queries = results?.find(({ type }) => type === "queries"); + let articles = results?.find(({ type }) => type === "articles"); + let products = results?.find(({ type }) => type === "products"); + + if (!totalResults) { + return ( +
    + +
    + ); + } + return ( +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + + {searchTerm.current && ( +
    + + View all results + + +
    + )} +
    +
    +
    + ); +} + +function NoResults({ + searchTerm, +}: { + searchTerm: MutableRefObject; +}) { + if (!searchTerm.current) { + return null; + } + return ( +

    + No results found for {searchTerm.current} +

    + ); +} From 85822079a27b233ac4428205ecdc2703196d8e0d Mon Sep 17 00:00:00 2001 From: hta218 Date: Mon, 16 Dec 2024 16:34:56 +0700 Subject: [PATCH 7/8] Handle go to search suggestions & simplify predictive searh --- .../predictive-search-result.tsx | 23 ++--- .../predictive-search-results.tsx | 96 ------------------- app/hooks/use-predictive-search.ts | 23 ++--- app/types/predictive-search.ts | 7 +- 4 files changed, 18 insertions(+), 131 deletions(-) delete mode 100644 app/components/layout/predictive-search/predictive-search-results.tsx diff --git a/app/components/layout/predictive-search/predictive-search-result.tsx b/app/components/layout/predictive-search/predictive-search-result.tsx index 4720bef1..711c55df 100644 --- a/app/components/layout/predictive-search/predictive-search-result.tsx +++ b/app/components/layout/predictive-search/predictive-search-result.tsx @@ -10,16 +10,11 @@ import { CompareAtPrice } from "~/components/compare-at-price"; import { getImageAspectRatio, isDiscounted } from "~/lib/utils"; type SearchResultTypeProps = { - goToSearchResult: (event: React.MouseEvent) => void; items?: NormalizedPredictiveSearchResultItem[]; type: NormalizedPredictiveSearchResults[number]["type"]; }; -export function PredictiveSearchResult({ - goToSearchResult, - items, - type, -}: SearchResultTypeProps) { +export function PredictiveSearchResult({ items, type }: SearchResultTypeProps) { let isSuggestions = type === "queries"; return ( @@ -36,11 +31,7 @@ export function PredictiveSearchResult({ )} > {items.map((item: NormalizedPredictiveSearchResultItem) => ( - + ))} ) : ( @@ -52,12 +43,11 @@ export function PredictiveSearchResult({ ); } -type SearchResultItemProps = Pick & { +type SearchResultItemProps = { item: NormalizedPredictiveSearchResultItem; }; function SearchResultItem({ - goToSearchResult, item: { id, __typename, @@ -74,8 +64,11 @@ function SearchResultItem({
  • {__typename === "Product" && ( diff --git a/app/components/layout/predictive-search/predictive-search-results.tsx b/app/components/layout/predictive-search/predictive-search-results.tsx deleted file mode 100644 index 3b59fa44..00000000 --- a/app/components/layout/predictive-search/predictive-search-results.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { ArrowRight } from "@phosphor-icons/react"; -import Link from "~/components/link"; -import { usePredictiveSearch } from "~/hooks/use-predictive-search"; -import { PredictiveSearchResult } from "./predictive-search-result"; - -export function PredictiveSearchResults() { - let { results, totalResults, searchTerm, searchInputRef } = - usePredictiveSearch(); - let queries = results?.find(({ type }) => type === "queries"); - let articles = results?.find(({ type }) => type === "articles"); - let products = results?.find(({ type }) => type === "products"); - - function goToSearchResult(event: React.MouseEvent) { - event.preventDefault(); - let type = event.currentTarget.dataset.type; - if (!searchInputRef.current) return; - if (type === "SearchQuerySuggestion") { - searchInputRef.current.value = event.currentTarget.innerText; - // dispatch event onchange for the search - searchInputRef.current.focus(); - } else { - searchInputRef.current.blur(); - searchInputRef.current.value = ""; - // close the aside - window.location.href = event.currentTarget.href; - } - } - - if (!totalResults) { - return ( -
    - -
    - ); - } - return ( -
    -
    -
    -
    - -
    -
    - -
    -
    -
    - - {searchTerm.current && ( -
    - - View all products - - -
    - )} -
    -
    -
    - ); -} - -function NoPredictiveSearchResults({ - searchTerm, -}: { - searchTerm: React.MutableRefObject; -}) { - if (!searchTerm.current) { - return null; - } - return ( -

    - No results found for {searchTerm.current} -

    - ); -} diff --git a/app/hooks/use-predictive-search.ts b/app/hooks/use-predictive-search.ts index fb7478fd..722abe0c 100644 --- a/app/hooks/use-predictive-search.ts +++ b/app/hooks/use-predictive-search.ts @@ -3,7 +3,6 @@ import { useEffect, useRef, useState } from "react"; import type { NormalizedPredictiveSearch, NormalizedPredictiveSearchResults, - UseSearchReturn, } from "~/types/predictive-search"; export const NO_PREDICTIVE_SEARCH_RESULTS: NormalizedPredictiveSearchResults = [ @@ -14,12 +13,14 @@ export const NO_PREDICTIVE_SEARCH_RESULTS: NormalizedPredictiveSearchResults = [ { type: "articles", items: [] }, ]; -export function usePredictiveSearch(): UseSearchReturn { - const fetchers = useFetchers(); - const searchTerm = useRef(""); - const searchInputRef = useRef(null); - const searchFetcher = fetchers.find((fetcher) => fetcher.data?.searchResults); +export function usePredictiveSearch(): NormalizedPredictiveSearch & { + searchTerm: React.MutableRefObject; +} { let [results, setResults] = useState(); + let fetchers = useFetchers(); + let searchTerm = useRef(""); + let searchFetcher = fetchers.find((fetcher) => fetcher.data?.searchResults); + useEffect(() => { if (searchFetcher) { setResults(searchFetcher.data?.searchResults); @@ -30,16 +31,10 @@ export function usePredictiveSearch(): UseSearchReturn { searchTerm.current = (searchFetcher.formData?.get("q") || "") as string; } - const search = (results || { + let search = (results || { results: NO_PREDICTIVE_SEARCH_RESULTS, totalResults: 0, }) as NormalizedPredictiveSearch; - // capture the search input element as a ref - useEffect(() => { - if (searchInputRef.current) return; - searchInputRef.current = document.querySelector('input[type="search"]'); - }, []); - - return { ...search, searchInputRef, searchTerm }; + return { ...search, searchTerm }; } diff --git a/app/types/predictive-search.ts b/app/types/predictive-search.ts index 8dbb3154..673c2158 100644 --- a/app/types/predictive-search.ts +++ b/app/types/predictive-search.ts @@ -4,11 +4,6 @@ import type { PredictiveProductFragment, } from "storefrontapi.generated"; -export type UseSearchReturn = NormalizedPredictiveSearch & { - searchInputRef: React.MutableRefObject; - searchTerm: React.MutableRefObject; -}; - type PredictiveSearchResultItemImage = | PredictiveCollectionFragment["image"] | PredictiveArticleFragment["image"] @@ -31,7 +26,7 @@ export type NormalizedPredictiveSearchResults = Array< >; export type NormalizedPredictiveSearchResultItem = { - __typename: string | undefined; + __typename?: "SearchQuerySuggestion" | "Product" | "Article"; handle: string; id: string; image?: PredictiveSearchResultItemImage; From d64eca2b43d063beef95c2f5adf8c8bffb8c2848 Mon Sep 17 00:00:00 2001 From: hta218 Date: Mon, 16 Dec 2024 16:40:34 +0700 Subject: [PATCH 8/8] Update search term on search page when loader data update --- app/routes/($locale).search.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/routes/($locale).search.tsx b/app/routes/($locale).search.tsx index 3fecff42..8960f8cd 100644 --- a/app/routes/($locale).search.tsx +++ b/app/routes/($locale).search.tsx @@ -9,7 +9,7 @@ import { import type { LoaderFunctionArgs, MetaArgs } from "@shopify/remix-oxygen"; import { defer } from "@shopify/remix-oxygen"; import { clsx } from "clsx"; -import { Fragment, Suspense, useRef } from "react"; +import { Fragment, Suspense, useEffect, useState } from "react"; import type { PaginatedProductsSearchQuery } from "storefrontapi.generated"; import { BreadCrumb } from "~/components/breadcrumb"; import Link from "~/components/link"; @@ -92,9 +92,13 @@ const POPULAR_SEARCHES = ["French Linen", "Shirt", "Cotton"]; export default function Search() { let { searchTerm, products, noResultRecommendations } = useLoaderData(); - let inputRef = useRef(null); + let [searchKey, setSearchKey] = useState(searchTerm); let noResults = products?.nodes?.length === 0; + useEffect(() => { + setSearchKey(searchTerm); + }, [searchTerm]); + return (
    @@ -121,9 +125,9 @@ export default function Search() { > setSearchKey(e.target.value)} name="q" placeholder="Search our store..." type="search" @@ -131,11 +135,7 @@ export default function Search() {