Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(seo): Add next/prev links in search page's infinite scroll. #811

Merged
merged 18 commits into from
Jul 13, 2021
Merged
4 changes: 2 additions & 2 deletions packages/gatsby-theme-store/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

"notification-bar.sale": "SELECTED ITEMS ON SALE! CHECK IT OUT!",

"search.page-list.more": "Show More",
"search.page-list.more.loading": "Loading...",
"search.page-list.next": "Show More",
"search.page-list.previous": "Previous Page",

"product.quantity.title": "Quantity",

Expand Down
4 changes: 2 additions & 2 deletions packages/gatsby-theme-store/i18n/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

"notification-bar.sale": "ITEM SELECIONADOS EM PROMOÇÃO! DÊ UMA OLHADA!",

"search.page-list.more": "Mostrar Mais",
"search.page-list.more.loading": "Carregando...",
"search.page-list.next": "Mostrar mais",
"search.page-list.previous": "Ver anteriores",

"product.quantity.title": "Quantidade",

Expand Down

This file was deleted.

93 changes: 84 additions & 9 deletions packages/gatsby-theme-store/src/components/Search/List/Page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,94 @@
import React from 'react'
import { gql } from '@vtex/gatsby-plugin-graphql'
import { Spinner, Center, Grid, Box } from '@vtex/store-ui'
import type { FC } from 'react'

import ProductSummary from '../../ProductSummary'
import type { ProductSummary_ProductFragment } from '../../ProductSummary/__generated__/ProductSummary_product.graphql'
import { useQuery } from '../../../sdk/graphql/useQuery'
import { useSearch } from '../../../sdk/search/useSearch'
import { SearchQuery } from './__generated__/SearchQuery.graphql'
import { useQueryVariablesFromSearchParams } from '../../../sdk/search/converter/useQueryVariablesFromSearchParams'
import type {
SearchQueryQuery,
SearchQueryQueryVariables,
} from './__generated__/SearchQuery.graphql'

interface Props {
products: Array<Maybe<ProductSummary_ProductFragment>>
columns: number[]
/** @description true if should display the page. This is used for prefetching a page */
display: boolean
cursor: number
initialData?: SearchQueryQuery
}

const Page: FC<Props> = ({ products }) => (
<>
{products.map((product) => (
<ProductSummary loading="lazy" key={product!.id!} product={product!} />
))}
</>
)
const Page: FC<Props> = ({ display, cursor, initialData, columns }) => {
const { searchParams, pageInfo } = useSearch()
const variables = useQueryVariablesFromSearchParams(
{
...searchParams,
page: cursor,
},
pageInfo
)

const { data } = useQuery<SearchQueryQuery, SearchQueryQueryVariables>({
...SearchQuery,
variables,
initialData,
revalidateOnMount: true,
})

if (display === false) {
return null
}

if (data == null) {
tlgimenes marked this conversation as resolved.
Show resolved Hide resolved
return (
<Box sx={{ height: ['200px', '500px'] }}>
<Center>
<Spinner />
</Center>
</Box>
)
}

return (
<Grid variant="search" columns={columns}>
{data.vtex.productSearch?.products?.map((product) => (
<ProductSummary loading="lazy" key={product!.id!} product={product!} />
))}
</Grid>
)
}

export const query = gql`
query SearchQuery(
$query: String
$map: String
$fullText: String
$selectedFacets: [VTEX_SelectedFacetInput!]
$from: Int
$to: Int
$orderBy: String
$hideUnavailableItems: Boolean = false
) {
vtex {
productSearch(
hideUnavailableItems: $hideUnavailableItems
selectedFacets: $selectedFacets
fullText: $fullText
query: $query
map: $map
from: $from
to: $to
orderBy: $orderBy
) {
products {
...ProductSummary_product
}
}
}
}
`

export default Page
131 changes: 59 additions & 72 deletions packages/gatsby-theme-store/src/components/Search/List/index.tsx
Original file line number Diff line number Diff line change
@@ -1,90 +1,77 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { gql } from '@vtex/gatsby-plugin-graphql'
/* eslint-disable @typescript-eslint/restrict-plus-operands */
import React from 'react'
import { useIntl } from '@vtex/gatsby-plugin-i18n'
import { Grid, UIButton } from '@vtex/store-ui'
import { UIButton } from '@vtex/store-ui'
import type { FC } from 'react'
import React, { Fragment } from 'react'

import { useSearchInfinite } from '../../../sdk/search/useSearchInfinite'
import OverlaySpinner from './OverlaySpinner'
import Page from './Page'
import { useSearch } from '../../../sdk/search/useSearch'
import type { SearchQueryQuery } from './__generated__/SearchQuery.graphql'
import { SearchQuery } from './__generated__/SearchQuery.graphql'

interface Props {
initialData: SearchQueryQuery | undefined
columns: number[]
pageSize?: number
initialData: SearchQueryQuery | undefined
}

const List: FC<Props> = ({ initialData, columns, pageSize }) => {
const List: FC<Props> = ({ columns, initialData }) => {
const { formatMessage } = useIntl()
const { data, fetchMore, isLoadingMore, isReachingEnd } = useSearchInfinite({
query: SearchQuery,
initialData,
pageSize,
})
const {
searchParams,
pageInfo: {
nextPage: next,
prevPage: previous,
pages,
addNextPage: setNextPage,
addPreviousPage: fetchPreviousPage,
},
} = useSearch()

const loadMoreLabel = formatMessage({ id: 'search.page-list.more' })
const loadingLabel = formatMessage({ id: 'search.page-list.more.loading' })

if (!data) {
return <OverlaySpinner />
}
const nextLabel = formatMessage({ id: 'search.page-list.next' })
const prevLabel = formatMessage({ id: 'search.page-list.previous' })

return (
<Fragment>
<Grid variant="search" columns={columns}>
{data.map((searchQuery, index) => (
<Page
key={`summary-page-${index}`}
products={searchQuery!.vtex.productSearch!.products!}
/>
))}
</Grid>
<UIButton
variant="loadMore"
onClick={(e: any) => {
e.target.blur?.()
fetchMore()
}}
aria-label={loadMoreLabel}
disabled={isReachingEnd || isLoadingMore}
>
{isReachingEnd ? '' : isLoadingMore ? loadingLabel : loadMoreLabel}
</UIButton>
</Fragment>
<>
{previous !== false && (
<UIButton
as="a"
variant="loadMore"
onClick={fetchPreviousPage}
aria-label={prevLabel}
{...{ href: previous.link, rel: 'prev' }}
tlgimenes marked this conversation as resolved.
Show resolved Hide resolved
>
{prevLabel}
</UIButton>
)}
{pages.map((page) => (
<Page
key={`search-result-page-${page}`}
initialData={page === searchParams.page ? initialData : undefined}
columns={columns}
cursor={page}
display
/>
))}
{next !== false && (
<UIButton
as="a"
variant="loadMore"
onClick={setNextPage}
aria-label={nextLabel}
{...{ href: next.link, rel: 'next' }}
>
{nextLabel}
</UIButton>
)}
{/* Prefetch Previous pages */}
{previous !== false && (
<Page columns={columns} cursor={previous.cursor} display={false} />
)}
{/* Prefetch Next page */}
{next !== false && (
<Page columns={columns} cursor={next.cursor} display={false} />
)}
</>
)
}

export const query = gql`
query SearchQuery(
$query: String
$map: String
$fullText: String
$selectedFacets: [VTEX_SelectedFacetInput!]
$from: Int
$to: Int
$orderBy: String
$hideUnavailableItems: Boolean = false
) {
vtex {
productSearch(
hideUnavailableItems: $hideUnavailableItems
selectedFacets: $selectedFacets
fullText: $fullText
query: $query
map: $map
from: $from
to: $to
orderBy: $orderBy
) {
products {
...ProductSummary_product
}
}
}
}
`

export default List
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'
import { BreadcrumbJsonLd, GatsbySeo } from 'gatsby-plugin-next-seo'
import type { FC } from 'react'

import { useMetadata } from '../../../sdk/seo/search/useMetadata'
import { useBreadcrumb } from '../../../sdk/seo/search/useBreadcrumbJsonLd'
import type { Options as MetadataOptions } from '../../../sdk/seo/search/useMetadata'
import type { Options as BreadcrumbOptions } from '../../../sdk/seo/search/useBreadcrumbJsonLd'

export type Props = MetadataOptions & BreadcrumbOptions

const SearchSEO: FC<Props> = (props) => {
const metadata = useMetadata(props)
const breadcrumb = useBreadcrumb(props)

return (
<>
<GatsbySeo {...metadata} defer />
{breadcrumb && <BreadcrumbJsonLd {...breadcrumb} defer />}
</>
)
}

export default SearchSEO
8 changes: 7 additions & 1 deletion packages/gatsby-theme-store/src/gatsby-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ interface StaticPath {
| 'NotFound'
}

const DEFAULT_PAGE_INFO = { size: 12 }

export const createPages = async ({
actions: { createPage, createRedirect },
graphql,
Expand Down Expand Up @@ -94,6 +96,7 @@ export const createPages = async ({
key,
value: segment,
})),
pageInfo: DEFAULT_PAGE_INFO,
}

createPage({
Expand Down Expand Up @@ -121,6 +124,7 @@ export const createPages = async ({
context: {
id,
canonicalPath: path,
pageInfo: DEFAULT_PAGE_INFO,
},
})
}
Expand All @@ -134,7 +138,9 @@ export const createPages = async ({
path: '/s/__client_side_search__',
matchPath: '/s/*',
component: resolve(__dirname, './src/templates/search.browser.tsx'),
context: {},
context: {
pageInfo: DEFAULT_PAGE_INFO,
},
})
}

Expand Down
4 changes: 3 additions & 1 deletion packages/gatsby-theme-store/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export { useNewsletter } from './sdk/newsletter/useNewsletter'
// GraphQL API
export { useLazyQuery } from './sdk/graphql/useLazyQuery'
export { useQuery } from './sdk/graphql/useQuery'
export { useQueryInfinite } from './sdk/graphql/useQueryInfinite'

// TODO: We should have a single solution for dealing with images in our framework
export { optimize } from './sdk/img/fileManager'
Expand Down Expand Up @@ -93,6 +92,9 @@ export type { Options as ProductJsonLdOptions } from './sdk/seo/product/useProdu
export { default as ProductSEO } from './components/Seo/product/ProductSEO'
export type { Props as ProductSEOProps } from './components/Seo/product/ProductSEO'

export type { Props as SearchSEOProps } from './components/Seo/search/SearchSEO'
export { default as SearchSEO } from './components/Seo/search/SearchSEO'

export { useProductPixel as useProductPixelEffect } from './sdk/product/useProductPixel'
export type { Options as ProductPixelOptions } from './sdk/product/useProductPixel'
export { default as RenderExtensionLoader } from './sdk/RenderExtensionLoader'
Expand Down
Loading