From 88f3bd653134857d478951aa4782c6993298121c Mon Sep 17 00:00:00 2001 From: Henrik Larsson Date: Fri, 11 Aug 2023 15:42:00 +0200 Subject: [PATCH] Iterated search experience --- app/[locale]/category/[slug]/page.tsx | 13 ++-- app/[locale]/globals.css | 4 +- app/[locale]/search/page.tsx | 17 +++++ components/layout/header/header.tsx | 2 +- components/layout/header/search/modal.tsx | 48 +++++++++++-- components/modules/hero/hero.tsx | 2 - components/product/product-view.tsx | 2 +- components/search/no-result.tsx | 50 +++++++++++++ components/search/search-result.tsx | 52 ++++++++++++++ components/search/search-root.tsx | 21 ++++++ components/search/search.tsx | 87 +++++++++++------------ messages/en.json | 3 + messages/sv.json | 3 + 13 files changed, 243 insertions(+), 61 deletions(-) create mode 100644 app/[locale]/search/page.tsx create mode 100644 components/search/no-result.tsx create mode 100644 components/search/search-result.tsx create mode 100644 components/search/search-root.tsx diff --git a/app/[locale]/category/[slug]/page.tsx b/app/[locale]/category/[slug]/page.tsx index 4cdfcd534..cdd56ed03 100644 --- a/app/[locale]/category/[slug]/page.tsx +++ b/app/[locale]/category/[slug]/page.tsx @@ -1,6 +1,7 @@ -import Text from 'components/ui/text/text'; -import { categoryQuery } from 'lib/sanity/queries'; -import { clientFetch } from 'lib/sanity/sanity.client'; +import Search from '@/components/search/search'; +import SearchResult from '@/components/search/search-result'; +import { categoryQuery } from '@/lib/sanity/queries'; +import { clientFetch } from '@/lib/sanity/sanity.client'; import { Metadata } from 'next'; import { notFound } from 'next/navigation'; @@ -34,8 +35,10 @@ export default async function ProductPage({ params }: CategoryPageParams) { const { title } = category; return ( -
- {title} +
+ + +
); } diff --git a/app/[locale]/globals.css b/app/[locale]/globals.css index e55b3c8b3..0e9b88ff0 100644 --- a/app/[locale]/globals.css +++ b/app/[locale]/globals.css @@ -77,7 +77,7 @@ body { /* DYNAMIC CONTENT MANAGER */ .dynamic-content > :not(.hero) { - @apply my-16 lg:my-24; + @apply my-12 md:my-16 lg:my-24; } .dynamic-content > :first-child { @@ -85,7 +85,7 @@ body { } .dynamic-content > :last-child { - @apply mb-16 lg:mb-24; + @apply mb-12 md:mb-16 lg:mb-24; } .dynamic-content .dynamic-content { diff --git a/app/[locale]/search/page.tsx b/app/[locale]/search/page.tsx new file mode 100644 index 000000000..2b879737d --- /dev/null +++ b/app/[locale]/search/page.tsx @@ -0,0 +1,17 @@ +'use client'; + +import Search from '@/components/search/search'; +import SearchResult from '@/components/search/search-result'; +import { useTranslations } from 'next-intl'; + +export default function SearchPage() { + const t = useTranslations('search'); + + return ( +
+ + + +
+ ); +} diff --git a/components/layout/header/header.tsx b/components/layout/header/header.tsx index fc3f07ca1..8f68d9bfe 100644 --- a/components/layout/header/header.tsx +++ b/components/layout/header/header.tsx @@ -47,7 +47,7 @@ export default async function Header({ locale }: HeaderProps) { {mainMenu.map((item: { title: string; slug: string }, i: number) => { return (
  • - + {item.title}
  • diff --git a/components/layout/header/search/modal.tsx b/components/layout/header/search/modal.tsx index 0a994d66a..5123f00d7 100644 --- a/components/layout/header/search/modal.tsx +++ b/components/layout/header/search/modal.tsx @@ -1,26 +1,64 @@ 'use client'; -import Search from '@/components/search/search'; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; +import Text from '@/components/ui/text/text'; import { useTranslations } from 'next-intl'; import { useState } from 'react'; import OpenSearch from './open-search'; +import { Highlight, Hits } from 'react-instantsearch'; + +import Search from '@/components/search/search'; +import Link from 'next/link'; + export default function SearchModal() { const [isOpen, setIsOpen] = useState(false); const t = useTranslations('search'); + const Hit = (props: any) => { + const { hit } = props; + const { handle, price } = props.hit; + + return ( + setIsOpen(!isOpen)} + href={`/product/${handle}`} + className="flex w-full gap-4 outline-offset-0" + > +
    +
    + + Brand + +

    + +

    +

    {price} SEK

    +
    + + ); + }; + return ( <> setIsOpen(!isOpen)}> - - + + - + {t('search')} - + + + diff --git a/components/modules/hero/hero.tsx b/components/modules/hero/hero.tsx index 6e2e6ec74..730e8b273 100644 --- a/components/modules/hero/hero.tsx +++ b/components/modules/hero/hero.tsx @@ -29,8 +29,6 @@ const heroSize = { const Hero = ({ variant, title, text, label, image, link }: HeroProps) => { const heroClass = heroSize[variant as HeroSize] || heroSize.fullScreen; - console.log(image); - return (
    +
    diff --git a/components/search/no-result.tsx b/components/search/no-result.tsx new file mode 100644 index 000000000..36577978a --- /dev/null +++ b/components/search/no-result.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { ReactNode } from 'react'; +import { ClearRefinements, useInstantSearch } from 'react-instantsearch'; + +import { useTranslations } from 'next-intl'; + +interface NoResultsProps { + children: ReactNode; + fallback: ReactNode; +} + +export function NoResultsBoundary({ children, fallback }: NoResultsProps) { + const { results } = useInstantSearch(); + + // The `__isArtificial` flag makes sure not to display the No Results message + // when no hits have been returned. + if (!results.__isArtificial && results.nbHits === 0) { + return ( + <> + {fallback} + + + ); + } + + return children; +} + +export function NoResults() { + const t = useTranslations('search'); + const { indexUiState } = useInstantSearch(); + + return ( +
    +

    + {t('noResults')} {indexUiState.query}. + +

    +
    + ); +} diff --git a/components/search/search-result.tsx b/components/search/search-result.tsx new file mode 100644 index 000000000..54d037d73 --- /dev/null +++ b/components/search/search-result.tsx @@ -0,0 +1,52 @@ +'use client'; + +import Text from '@/components/ui/text/text'; +import { cn } from '@/lib/utils'; +import { useTranslations } from 'next-intl'; +import Link from 'next/link'; +import { Configure, Highlight, InfiniteHits } from 'react-instantsearch'; + +export default function SearchResult() { + const t = useTranslations('search'); + + const Hit = (props: any) => { + const { hit } = props; + const { handle, price } = props.hit; + + return ( + +
    +
    + + Brand + +

    + +

    +

    {price} SEK

    +
    + + ); + }; + + return ( + <> + + + + ); +} diff --git a/components/search/search-root.tsx b/components/search/search-root.tsx new file mode 100644 index 000000000..4a568d367 --- /dev/null +++ b/components/search/search-root.tsx @@ -0,0 +1,21 @@ +import algoliasearch from 'algoliasearch/lite'; +import { InstantSearch } from 'react-instantsearch'; + +const searchClient = algoliasearch( + `${process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID}`, + `${process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_API_KEY}` +); + +interface SearchRootProps { + children: JSX.Element | JSX.Element[]; +} + +export default function SearchRoot({ children }: SearchRootProps) { + return ( + <> + + {children} + + + ); +} diff --git a/components/search/search.tsx b/components/search/search.tsx index 1ff779908..b882823e9 100644 --- a/components/search/search.tsx +++ b/components/search/search.tsx @@ -1,43 +1,48 @@ -import algoliasearch from 'algoliasearch/lite'; -// import { useLocale } from 'next-intl'; +'use client'; + import Text from '@/components/ui/text'; -import { Highlight, Hits, InstantSearch, SearchBox } from 'react-instantsearch'; -const searchClient = algoliasearch( - `${process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID}`, - `${process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_API_KEY}` -); +import { cn } from 'lib/utils'; +import { useTranslations } from 'next-intl'; +import SearchRoot from './search-root'; + +import { SearchBox } from 'react-instantsearch'; + +import { ReactNode } from 'react'; +import { NoResults, NoResultsBoundary } from './no-result'; + +interface SearchProps { + title?: string; + placeholder?: string; + className?: string; + children: ReactNode; + isCategory?: boolean; +} + +export default function Search({ title, placeholder, children, isCategory = false }: SearchProps) { + const t = useTranslations('search'); + + console.log(isCategory); -export default function Search() { - // const locale = useLocale(); - // Hit. - function Hit(props: any) { - return ( -
  • - -
  • - ); - } return ( -
    - - {/* Widgets */} + + {/* Search top */} +
    + {title && ( + + {title} + + )} + +
    - {/* */} - - -
    -
    + }>{children} + ); } diff --git a/messages/en.json b/messages/en.json index a4f7706fa..fa6d6beae 100644 --- a/messages/en.json +++ b/messages/en.json @@ -22,6 +22,9 @@ "submitTitle": "Submit your search query", "clearTitle": "Clear your search query", "resetTitle": "Reset your search query", + "noResults": "No results for", + "showMore": "Show more results", + "searchCategory": "Search in category", "seo": { "title": "Search", "description": "Search for product or category" diff --git a/messages/sv.json b/messages/sv.json index 4728f3112..d381b2ddd 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -22,6 +22,9 @@ "submitTitle": "Skicka din sökfråga", "clearTitle": "Rensa din sökfråga", "resetTitle": "Återställ din sökfråga", + "noResults": "Inga resultat för", + "showMore": "Visa fler resultat", + "searchCategory": "Sök i kategori", "seo": { "title": "Sök", "description": "Sök efter produkt eller kategori"