diff --git a/app/components/breadcrumb.tsx b/app/components/breadcrumb.tsx new file mode 100644 index 00000000..d4e72cda --- /dev/null +++ b/app/components/breadcrumb.tsx @@ -0,0 +1,18 @@ +import { cn } from "~/lib/cn"; +import { Link } from "./link"; + +export function BreadCrumb({ + homeLabel = "Home", + page, + className, +}: { homeLabel?: string; page: string; className?: string }) { + return ( +
+ + {homeLabel} + + / + {page} +
+ ); +} diff --git a/app/components/layout/predictive-search/index.tsx b/app/components/layout/predictive-search/index.tsx index af1e92c8..443f3a91 100644 --- a/app/components/layout/predictive-search/index.tsx +++ b/app/components/layout/predictive-search/index.tsx @@ -57,7 +57,6 @@ function PredictiveSearch() { onClear={fetchResults} placeholder="Enter a keyword" ref={inputRef} - className="rounded" autoComplete="off" prefixElement={ diff --git a/app/components/overlay-and-background.tsx b/app/components/overlay-and-background.tsx index be8e84f0..b0f66398 100644 --- a/app/components/overlay-and-background.tsx +++ b/app/components/overlay-and-background.tsx @@ -4,8 +4,8 @@ import type { OverlayProps } from "./overlay"; import { Overlay } from "./overlay"; export interface OverlayAndBackgroundProps - extends BackgroundImageProps, - OverlayProps {} + extends Partial, + Partial {} export function OverlayAndBackground(props: OverlayAndBackgroundProps) { let { diff --git a/app/components/section.tsx b/app/components/section.tsx index 3ce78d23..250375c8 100644 --- a/app/components/section.tsx +++ b/app/components/section.tsx @@ -22,14 +22,14 @@ export type BackgroundProps = BackgroundImageProps & { export interface SectionProps extends Omit, "padding">, - Omit, "children">, + Partial, "children">>, Omit, "children">, Partial, - OverlayProps { - as: React.ElementType; - borderRadius: number; - containerClassName: string; - children: React.ReactNode; + Partial { + as?: React.ElementType; + borderRadius?: number; + containerClassName?: string; + children?: React.ReactNode; } let variants = cva("relative", { diff --git a/app/routes/($locale).search.tsx b/app/routes/($locale).search.tsx index 5cc1379b..8090f3a2 100644 --- a/app/routes/($locale).search.tsx +++ b/app/routes/($locale).search.tsx @@ -1,3 +1,4 @@ +import { MagnifyingGlass, X } from "@phosphor-icons/react"; import { Await, Form, useLoaderData } from "@remix-run/react"; import { Analytics, @@ -7,15 +8,17 @@ import { } from "@shopify/hydrogen"; import type { LoaderFunctionArgs, MetaArgs } from "@shopify/remix-oxygen"; import { defer } from "@shopify/remix-oxygen"; -import { Suspense } from "react"; +import { clsx } from "clsx"; +import { Fragment, Suspense, useRef } from "react"; +import type { PaginatedProductsSearchQuery } from "storefrontapi.generated"; +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 { PRODUCT_CARD_FRAGMENT } from "~/data/fragments"; import { PAGINATION_SIZE, getImageLoadingPriority } from "~/lib/const"; import { seoPayload } from "~/lib/seo.server"; -import { Heading, PageHeader, Section, Text } from "~/modules/text"; -import { Grid } from "~/modules/grid"; -import { Input } from "~/modules/input"; -import { ProductCard } from "~/components/product/product-card"; -import { ProductSwimlane } from "~/modules/product-swimlane"; import { type FeaturedData, getFeaturedData, @@ -25,46 +28,56 @@ export async function loader({ request, context: { storefront }, }: LoaderFunctionArgs) { - let searchParams = new URL(request.url).searchParams; + let { searchParams } = new URL(request.url); let searchTerm = searchParams.get("q"); - let variables = getPaginationVariables(request, { - pageBy: PAGINATION_SIZE, - }); + let products: PaginatedProductsSearchQuery["products"] = null; - let { products } = await storefront.query(SEARCH_QUERY, { - variables: { - searchTerm, - ...variables, - country: storefront.i18n.country, - language: storefront.i18n.language, - }, - }); - - let shouldGetRecommendations = !searchTerm || products?.nodes?.length === 0; + if (searchTerm) { + let variables = getPaginationVariables(request, { + pageBy: PAGINATION_SIZE, + }); - let seo = seoPayload.collection({ - url: request.url, - collection: { - id: "search", - title: "Search", - handle: "search", - descriptionHtml: "Search results", - description: "Search results", - seo: { - title: "Search", - description: `Showing ${products.nodes.length} search results for "${searchTerm}"`, + let data = await storefront.query( + SEARCH_QUERY, + { + variables: { + searchTerm, + ...variables, + country: storefront.i18n.country, + language: storefront.i18n.language, + }, }, - metafields: [], - products, - updatedAt: new Date().toISOString(), - }, - }); + ); + products = data.products; + } + + let noResults = products?.nodes?.length === 0; return defer({ - seo, + seo: seoPayload.collection({ + url: request.url, + collection: { + id: "search", + title: "Search", + handle: "search", + descriptionHtml: "Search results", + description: "Search results", + seo: { + title: "Search", + description: noResults + ? searchTerm + ? `No results found for "${searchTerm}"` + : "Search our store" + : `Showing ${products.nodes.length} search results for "${searchTerm}"`, + }, + metafields: [], + products, + updatedAt: new Date().toISOString(), + }, + }), searchTerm, products, - noResultRecommendations: shouldGetRecommendations + noResultRecommendations: noResults ? getNoResultRecommendations(storefront) : Promise.resolve(null), }); @@ -74,88 +87,129 @@ export let meta = ({ matches }: MetaArgs) => { return getSeoMeta(...matches.map((match) => (match.data as any).seo)); }; +const POPULAR_SEARCHES = ["French Linen", "Shirt", "Cotton"]; + export default function Search() { let { searchTerm, products, noResultRecommendations } = useLoaderData(); + let inputRef = useRef(null); let noResults = products?.nodes?.length === 0; return ( - <> - - - Search - -
- - -
-
- {!searchTerm || noResults ? ( +
+ +

Search

+
+ Popular Searches: + {POPULAR_SEARCHES.map((search, ind) => ( + + + {search} + + {ind < POPULAR_SEARCHES.length - 1 && ( + , + )} + + ))} +
+
+ + + + + {noResults ? ( ) : ( -
- - {({ nodes, isLoading, NextLink, PreviousLink }) => { - let itemsMarkup = nodes.map((product, i) => ( - - )); - - return ( - <> -
- - {isLoading ? "Loading..." : "Previous"} - -
- {itemsMarkup} -
- - {isLoading ? "Loading..." : "Next"} - -
- - ); - }} -
-
+ + {({ + nodes, + isLoading, + nextPageUrl, + hasNextPage, + previousPageUrl, + hasPreviousPage, + }) => { + return ( +
+ {hasPreviousPage && ( + + {isLoading ? "Loading..." : "Previous"} + + )} +
+ {nodes.map((product, idx) => ( + + ))} +
+ {hasNextPage && ( + + {isLoading ? "Loading..." : "Next"} + + )} +
+ ); + }} +
)} - +
); } function NoResults({ - noResults, + searchTerm, recommendations, }: { - noResults: boolean; + searchTerm: string; recommendations: Promise; }) { return ( <> - {noResults && ( -
- - No results, try a different search. - -
+ {searchTerm && ( +
+ No results for "{searchTerm}", try a different search. +
)} {(result) => { - if (!result) return null; + if (!result) { + return null; + } let { featuredProducts } = result; - return ( - <> - - +
+

Trending Products

+ + {featuredProducts.nodes.map((product) => ( + + ))} + +
); }}
@@ -182,7 +243,7 @@ function NoResults({ } export function getNoResultRecommendations( - storefront: LoaderFunctionArgs["context"]["storefront"] + storefront: LoaderFunctionArgs["context"]["storefront"], ) { return getFeaturedData(storefront, { pageBy: PAGINATION_SIZE }); } @@ -218,4 +279,4 @@ let SEARCH_QUERY = `#graphql } ${PRODUCT_CARD_FRAGMENT} -` as let; +` as const; diff --git a/app/sections/all-products.tsx b/app/sections/all-products.tsx index 4b60b238..270b5323 100644 --- a/app/sections/all-products.tsx +++ b/app/sections/all-products.tsx @@ -4,6 +4,7 @@ import type { HydrogenComponentSchema } from "@weaverse/hydrogen"; import clsx from "clsx"; import { forwardRef } from "react"; import type { AllProductsQuery } from "storefrontapi.generated"; +import { BreadCrumb } from "~/components/breadcrumb"; import Link from "~/components/link"; import { ProductCard } from "~/components/product/product-card"; import { Section, type SectionProps, layoutInputs } from "~/components/section"; @@ -21,6 +22,7 @@ let AllProducts = forwardRef((props, ref) => { return (
+

{heading}

{({ diff --git a/app/sections/collection-filters/index.tsx b/app/sections/collection-filters/index.tsx index 376f836a..0ff062f0 100644 --- a/app/sections/collection-filters/index.tsx +++ b/app/sections/collection-filters/index.tsx @@ -1,13 +1,13 @@ import { useLoaderData } from "@remix-run/react"; +import { Image } from "@shopify/hydrogen"; import type { HydrogenComponentSchema } from "@weaverse/hydrogen"; import { forwardRef, useEffect, useState } from "react"; import type { CollectionDetailsQuery } from "storefrontapi.generated"; -import Link from "~/components/link"; +import { BreadCrumb } from "~/components/breadcrumb"; import { Section, type SectionProps, layoutInputs } from "~/components/section"; import { Filters } from "./filters"; import { ProductsPagination } from "./products-pagination"; import { ToolsBar } from "./tools-bar"; -import { Image } from "@shopify/hydrogen"; export interface CollectionFiltersData { showBreadcrumb: boolean; @@ -79,13 +79,7 @@ let CollectionFilters = forwardRef(
{showBreadcrumb && ( -
- - Home - - / - {collection.title} -
+ )}

{collection.title}

{showDescription && collection.description && (