Skip to content

Commit

Permalink
Merge pull request Weaverse#244 from Weaverse/dev
Browse files Browse the repository at this point in the history
Update search page & predictive search, simplify structure, fix minor bugs
  • Loading branch information
hta218 authored Dec 16, 2024
2 parents 3c8b826 + a077f80 commit dbdb276
Show file tree
Hide file tree
Showing 16 changed files with 312 additions and 537 deletions.
130 changes: 103 additions & 27 deletions app/components/layout/predictive-search/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import { MagnifyingGlass } 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 { Input } from "~/modules/input";
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 (
<Dialog.Root>
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger
asChild
className="hidden lg:flex h-8 w-8 items-center justify-center focus-visible:outline-none"
Expand Down Expand Up @@ -36,37 +47,102 @@ export function PredictiveSearchButton() {
<VisuallyHidden.Root asChild>
<Dialog.Title>Predictive search</Dialog.Title>
</VisuallyHidden.Root>
<PredictiveSearch />
<div className="relative pt-[--topbar-height]">
<PredictiveSearchForm>
{({ fetchResults, inputRef }) => (
<div className="flex items-center gap-3 w-[560px] max-w-[90vw] mx-auto px-3 my-6 border border-line-subtle">
<MagnifyingGlass className="h-5 w-5 shrink-0 text-gray-500" />
<input
name="q"
type="search"
onChange={(e) => 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"
/>
<button
type="button"
className="shrink-0 text-gray-500 p-1"
onClick={() => {
if (inputRef.current) {
inputRef.current.value = "";
fetchResults("");
}
}}
>
<X className="w-5 h-5" />
</button>
</div>
)}
</PredictiveSearchForm>
<PredictiveSearchResults />
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

function PredictiveSearch() {
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 (
<div className="absolute top-full z-10 flex w-full items-center justify-center">
<NoResults searchTerm={searchTerm} />
</div>
);
}
return (
<div className="relative pt-[--topbar-height]">
<PredictiveSearchForm>
{({ fetchResults, inputRef }) => (
<div className="mx-auto w-full max-w-[560px] p-6">
<Input
name="q"
type="search"
onChange={fetchResults}
onFocus={fetchResults}
onClear={fetchResults}
placeholder="Enter a keyword"
ref={inputRef}
autoComplete="off"
prefixElement={
<MagnifyingGlass className="h-5 w-5 shrink-0 text-gray-500" />
}
autoFocus={true}
/>
<div className="absolute left-1/2 top-full z-10 flex w-fit -translate-x-1/2 items-center justify-center">
<div className="grid w-screen min-w-[430px] max-w-[720px] grid-cols-1 gap-6 bg-[--color-header-bg] p-6 lg:grid-cols-[1fr_2fr] max-h-[80vh] overflow-y-auto">
<div className="space-y-8">
<div className="flex flex-col gap-4 divide-y divide-line">
<PredictiveSearchResult type="queries" items={queries?.items} />
</div>
<div className="flex flex-col gap-4">
<PredictiveSearchResult type="articles" items={articles?.items} />
</div>
)}
</PredictiveSearchForm>
<PredictiveSearchResults />
</div>
<div className="space-y-6">
<PredictiveSearchResult
type="products"
items={products?.items?.slice(0, 5)}
/>
{searchTerm.current && (
<div>
<Link
to={`/search?q=${searchTerm.current}`}
variant="underline"
className="flex items-center gap-2 w-fit"
>
<span>View all results</span>
<ArrowRight className="w-4 h-4" />
</Link>
</div>
)}
</div>
</div>
</div>
);
}

function NoResults({
searchTerm,
}: {
searchTerm: MutableRefObject<string>;
}) {
if (!searchTerm.current) {
return null;
}
return (
<p className="w-[640px] shadow-header bg-background p-6">
No results found for <q>{searchTerm.current}</q>
</p>
);
}
147 changes: 91 additions & 56 deletions app/components/layout/predictive-search/predictive-search-result.tsx
Original file line number Diff line number Diff line change
@@ -1,83 +1,118 @@
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";

export function PredictiveSearchResult({
goToSearchResult,
items,
searchTerm,
type,
}: SearchResultTypeProps) {
type SearchResultTypeProps = {
items?: NormalizedPredictiveSearchResultItem[];
type: NormalizedPredictiveSearchResults[number]["type"];
};

export function PredictiveSearchResult({ items, type }: SearchResultTypeProps) {
let isSuggestions = type === "queries";
let categoryUrl = `/search?q=${
searchTerm.current
}&type=${pluralToSingularSearchType(type)}`;

return (
<div
key={type}
className="predictive-search-result flex flex-col gap-4 divide-y divide-line-subtle"
>
<Link
prefetch="intent"
className="uppercase font-bold"
to={categoryUrl}
onClick={goToSearchResult}
>
<div key={type} className="predictive-search-result flex flex-col gap-4">
<div className="uppercase font-bold border-b border-line-subtle pb-3">
{isSuggestions ? "Suggestions" : type}
</Link>
{items?.length && (
</div>
{items?.length ? (
<ul
className={clsx(
"pt-5",
type === "queries" && "space-y-1",
type === "articles" && "space-y-3",
type === "products" && "space-y-4",
)}
>
{items.map((item: NormalizedPredictiveSearchResultItem) => (
<SearchResultItem
goToSearchResult={goToSearchResult}
item={item}
key={item.id}
/>
<SearchResultItem item={item} key={item.id} />
))}
</ul>
) : (
<div className="text-body-subtle">
No {isSuggestions ? "suggestions" : type} available.
</div>
)}
</div>
);
}

/**
* 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<NormalizedPredictiveSearchResults[number]["type"]>,
) {
let plural = {
articles: "ARTICLE",
collections: "COLLECTION",
pages: "PAGE",
products: "PRODUCT",
queries: "QUERY",
};

if (typeof type === "string") {
return plural[type];
}
type SearchResultItemProps = {
item: NormalizedPredictiveSearchResultItem;
};

return type.map((t) => plural[t]).join(",");
function SearchResultItem({
item: {
id,
__typename,
image,
compareAtPrice,
price,
title,
url,
vendor,
styledTitle,
},
}: SearchResultItemProps) {
return (
<li key={id}>
<Link
className="flex gap-4"
to={
__typename === "SearchQuerySuggestion" || !url
? `/search?q=${id}`
: url
}
data-type={__typename}
>
{__typename === "Product" && (
<div className="h-20 w-20 shrink-0">
{image?.url && (
<Image
alt={image.altText ?? ""}
src={image.url}
width={200}
height={200}
aspectRatio={getImageAspectRatio(image, "adapt")}
className="h-full w-full object-cover object-center animate-fade-in"
/>
)}
</div>
)}
<div className="space-y-1">
{vendor && (
<div className="text-body-subtle text-sm">By {vendor}</div>
)}
{styledTitle ? (
<div
className="reveal-underline"
dangerouslySetInnerHTML={{ __html: styledTitle }}
/>
) : (
<div
className={clsx(
__typename === "Product" ? "line-clamp-1" : "line-clamp-2",
)}
>
<span className="reveal-underline">{title}</span>
</div>
)}
{price && (
<div className="flex gap-2 text-sm">
<Money withoutTrailingZeros data={price as MoneyV2} />
{isDiscounted(price as MoneyV2, compareAtPrice as MoneyV2) && (
<CompareAtPrice data={compareAtPrice as MoneyV2} />
)}
</div>
)}
</div>
</Link>
</li>
);
}
Loading

0 comments on commit dbdb276

Please sign in to comment.