From 737b544521e4f7fdf7cbf3c12f848ddf82ff74c2 Mon Sep 17 00:00:00 2001 From: Thomas Cristina de Carvalho Date: Fri, 12 Jan 2024 16:58:16 -0500 Subject: [PATCH] Add collection template --- .../app/components/CmsSection.tsx | 2 + .../app/components/collection/SortFilter.tsx | 10 +- .../app/components/product/ProductCard.tsx | 9 +- .../components/product/ProductCardGrid.tsx | 23 +- .../sections/CollectionBannerSection.tsx | 47 +++ .../sections/CollectionProductGridSection.tsx | 152 +++++++++ .../sections/FeaturedCollectionSection.tsx | 8 +- .../hydrogen-theme/app/graphql/queries.ts | 35 +- .../app/lib/resolveShopifyPromises.ts | 112 ++++++- .../app/lib/sectionRelsolver.ts | 12 + .../app/lib/shopifyCollection.ts | 141 ++++++++ templates/hydrogen-theme/app/qroq/queries.ts | 101 ++++-- templates/hydrogen-theme/app/qroq/sections.ts | 53 ++- .../hydrogen-theme/app/routes/($locale).$.tsx | 1 + ...$locale).collections.$collectionHandle.tsx | 308 ++++-------------- .../($locale).products.$productHandle.tsx | 5 +- .../hydrogen-theme/app/routes/_index.tsx | 3 +- .../storefrontapi.generated.d.ts | 38 ++- .../schemas/documents/collectionTemplate.tsx | 4 + .../hydrogen-theme/studio/schemas/index.ts | 10 +- .../schemas/objects/global/sectionsList.ts | 22 ++ .../objects/sections/collectionBanner.tsx | 33 ++ .../sections/collectionProductGrid.tsx | 67 ++++ .../static/assets/collectionBannerSection.png | Bin 0 -> 14189 bytes .../assets/collectionProductGridSection.png | Bin 0 -> 30122 bytes 25 files changed, 863 insertions(+), 333 deletions(-) create mode 100644 templates/hydrogen-theme/app/components/sections/CollectionBannerSection.tsx create mode 100644 templates/hydrogen-theme/app/components/sections/CollectionProductGridSection.tsx create mode 100644 templates/hydrogen-theme/app/lib/shopifyCollection.ts create mode 100644 templates/hydrogen-theme/studio/schemas/objects/sections/collectionBanner.tsx create mode 100644 templates/hydrogen-theme/studio/schemas/objects/sections/collectionProductGrid.tsx create mode 100644 templates/hydrogen-theme/studio/static/assets/collectionBannerSection.png create mode 100644 templates/hydrogen-theme/studio/static/assets/collectionProductGridSection.png diff --git a/templates/hydrogen-theme/app/components/CmsSection.tsx b/templates/hydrogen-theme/app/components/CmsSection.tsx index 63aeeb9..05c2eb8 100644 --- a/templates/hydrogen-theme/app/components/CmsSection.tsx +++ b/templates/hydrogen-theme/app/components/CmsSection.tsx @@ -5,6 +5,7 @@ import {Suspense, useMemo} from 'react'; import type {FOOTERS_FRAGMENT} from '~/qroq/footers'; import type { + COLLECTION_SECTIONS_FRAGMENT, PRODUCT_SECTIONS_FRAGMENT, SECTIONS_FRAGMENT, } from '~/qroq/sections'; @@ -14,6 +15,7 @@ import {useSettingsCssVars} from '~/hooks/useSettingsCssVars'; import {sections} from '~/lib/sectionRelsolver'; type CmsSectionsProps = + | NonNullable>[0] | NonNullable> | NonNullable>[0] | NonNullable>[0]; diff --git a/templates/hydrogen-theme/app/components/collection/SortFilter.tsx b/templates/hydrogen-theme/app/components/collection/SortFilter.tsx index 87913ff..bd00e4e 100644 --- a/templates/hydrogen-theme/app/components/collection/SortFilter.tsx +++ b/templates/hydrogen-theme/app/components/collection/SortFilter.tsx @@ -46,17 +46,11 @@ export type SortParam = type Props = { appliedFilters?: AppliedFilter[]; children: React.ReactNode; - collections?: Array<{handle: string; title: string}>; filters: Filter[]; }; export const FILTER_URL_PREFIX = 'filter.'; -export function SortFilter({ - appliedFilters = [], - children, - collections = [], - filters, -}: Props) { +export function SortFilter({appliedFilters = [], children, filters}: Props) { const [isOpen, setIsOpen] = useState(false); return ( <> @@ -71,7 +65,7 @@ export function SortFilter({ -
+
0 ? products.map((product) => (
  • - +
  • )) : skeleton ? [...Array(skeleton.cardsNumber ?? 3)].map((_, i) => (
  • ; + +export function CollectionBannerSection( + props: SectionDefaultProps & {data: CollectionBannerSectionProps}, +) { + const loaderData = useLoaderData(); + const collection = loaderData.collection; + + return collection ? ( +
    + {/* Todo => add settings for banner height */} + {/* Todo => add setting to add overlay */} + {/* Todo => add settings for text and content alignment */} +
    + {props.data.showImage && collection.image && ( + + )} +
    +
    +
    +

    {collection.title}

    + {props.data.showDescription &&

    {collection.description}

    } +
    +
    +
    +
    +
    + ) : null; +} diff --git a/templates/hydrogen-theme/app/components/sections/CollectionProductGridSection.tsx b/templates/hydrogen-theme/app/components/sections/CollectionProductGridSection.tsx new file mode 100644 index 0000000..e8968ae --- /dev/null +++ b/templates/hydrogen-theme/app/components/sections/CollectionProductGridSection.tsx @@ -0,0 +1,152 @@ +import type {Filter} from '@shopify/hydrogen/storefront-api-types'; +import type {TypeFromSelection} from 'groqd'; +import type { + CollectionProductGridQuery, + ProductCardFragment, +} from 'storefrontapi.generated'; + +import { + Await, + useLoaderData, + useNavigate, + useSearchParams, +} from '@remix-run/react'; +import {Pagination, flattenConnection} from '@shopify/hydrogen'; +import {Suspense, useEffect} from 'react'; + +import type {SectionDefaultProps} from '~/lib/type'; +import type {COLLECTION_PRODUCT_GRID_SECTION_FRAGMENT} from '~/qroq/sections'; +import type {loader} from '~/routes/($locale).collections.$collectionHandle'; + +import {useLocale} from '~/hooks/useLocale'; +import {getAppliedFilters} from '~/lib/shopifyCollection'; + +import {SortFilter} from '../collection/SortFilter'; +import {ProductCardGrid} from '../product/ProductCardGrid'; + +type CollectionProductGridSectionProps = TypeFromSelection< + typeof COLLECTION_PRODUCT_GRID_SECTION_FRAGMENT +>; + +export type ShopifyCollection = CollectionProductGridQuery['collection']; + +export function CollectionProductGridSection( + props: SectionDefaultProps & {data: CollectionProductGridSectionProps}, +) { + const locale = useLocale(); + const [searchParams] = useSearchParams(); + const loaderData = useLoaderData(); + const collectionProductGridPromise = loaderData?.collectionProductGridPromise; + const columns = props.data.desktopColumns; + const mobileColumns = props.data.mobileColumns; + + // Todo => Add skeleton and errorElement + return ( + + Error
  • } + resolve={collectionProductGridPromise} + > + {(result) => { + const collection = result?.collection as ShopifyCollection; + + if (!collection) { + return null; + } + + const appliedFilters = getAppliedFilters({ + collection, + locale, + searchParams, + }); + + return ( +
    + + + {({ + NextLink, + PreviousLink, + hasNextPage, + isLoading, + nextPageUrl, + nodes, + state, + }) => ( + <> +
    + + {isLoading ? 'Loading...' : 'Load previous'} + +
    + +
    + + {isLoading ? 'Loading...' : 'Load more products'} + +
    + + )} +
    +
    +
    + ); + }} + + + ); +} + +function ProductsLoadedOnScroll({ + columns, + hasNextPage, + inView, + nextPageUrl, + nodes, + state, +}: { + columns?: { + desktop?: null | number; + mobile?: null | number; + }; + hasNextPage: boolean; + inView: boolean; + nextPageUrl: string; + nodes: ProductCardFragment[]; + state: unknown; +}) { + const navigate = useNavigate(); + + useEffect(() => { + if (inView && hasNextPage) { + navigate(nextPageUrl, { + preventScrollReset: true, + replace: true, + state, + }); + } + }, [inView, navigate, state, nextPageUrl, hasNextPage]); + + return ( + + ); +} diff --git a/templates/hydrogen-theme/app/components/sections/FeaturedCollectionSection.tsx b/templates/hydrogen-theme/app/components/sections/FeaturedCollectionSection.tsx index 88ec8fa..33fc28c 100644 --- a/templates/hydrogen-theme/app/components/sections/FeaturedCollectionSection.tsx +++ b/templates/hydrogen-theme/app/components/sections/FeaturedCollectionSection.tsx @@ -39,7 +39,9 @@ export function FeaturedCollectionSection( > {(products) => ( )} @@ -52,7 +54,9 @@ function Skeleton(props: {cardsNumber: number; columns: number}) { return (
    ; type SanityProductData = InferType; +type SanityCollectionData = InferType; type PromiseResolverArgs = { - document: {data: SanityPageData | SanityProductData}; + document: {data: SanityCollectionData | SanityPageData | SanityProductData}; + request: Request; storefront: Storefront; }; @@ -31,43 +36,81 @@ type PromiseResolverArgs = { */ export function resolveShopifyPromises({ document, + request, storefront, }: PromiseResolverArgs) { const featuredCollectionPromise = resolveFeaturedCollectionPromise({ document, + request, storefront, }); const collectionListPromise = resolveCollectionListPromise({ document, + request, storefront, }); const featuredProductPromise = resolveFeaturedProductPromise({ document, + request, storefront, }); const relatedProductsPromise = resolveRelatedProductsPromise({ document, + request, + storefront, + }); + + const collectionProductGridPromise = resolveCollectionProductGridPromise({ + document, + request, storefront, }); return { collectionListPromise, + collectionProductGridPromise, featuredCollectionPromise, featuredProductPromise, relatedProductsPromise, }; } +function getSections(document: { + data: SanityCollectionData | SanityPageData | SanityProductData; +}) { + if (document?.data?._type === 'page' || document?.data?._type === 'home') { + return document.data.sections; + } + + if (document?.data?._type === 'product') { + return ( + document.data?.product?.template?.sections || + document.data?.defaultProductTemplate?.sections + ); + } + + if (document?.data?._type === 'collection') { + return ( + document.data?.collection?.template?.sections || + document.data?.defaultCollectionTemplate?.sections + ); + } + + return []; +} + function resolveFeaturedCollectionPromise({ document, storefront, }: PromiseResolverArgs) { const promises: Promise[] = []; - for (const section of document.data?.sections || []) { + const sections = getSections(document); + + for (const section of sections || []) { if (section._type === 'featuredCollectionSection') { const gid = section.collection?.store.gid; const first = section.maxProducts || 3; @@ -106,7 +149,9 @@ function resolveCollectionListPromise({ }: PromiseResolverArgs) { const promises: Promise[] = []; - for (const section of document.data?.sections || []) { + const sections = getSections(document); + + for (const section of sections || []) { if (section._type === 'collectionListSection') { const first = section.collections?.length; const ids = section.collections?.map( @@ -148,7 +193,9 @@ function resolveFeaturedProductPromise({ }: PromiseResolverArgs) { const promises: Promise[] = []; - for (const section of document.data?.sections || []) { + const sections = getSections(document); + + for (const section of sections || []) { if (section._type === 'featuredProductSection') { const gid = section.product?.store.gid; @@ -189,9 +236,14 @@ async function resolveRelatedProductsPromise({ return null; } - const productId = document.data?.store.gid; + const productId = document.data?.product?.store.gid; + const sections = getSections(document); - for (const section of document.data?.sections || []) { + if (!productId) { + return null; + } + + for (const section of sections || []) { if (section._type === 'relatedProductsSection') { promise = storefront.query(RECOMMENDED_PRODUCTS_QUERY, { variables: { @@ -206,3 +258,47 @@ async function resolveRelatedProductsPromise({ return promise || null; } + +async function resolveCollectionProductGridPromise({ + document, + request, + storefront, +}: PromiseResolverArgs) { + let promise; + + if (document.data?._type !== 'collection') { + return null; + } + + const collectionId = document.data?.collection?.store.gid; + + if (!collectionId) { + return null; + } + + const sections = getSections(document); + const searchParams = new URL(request.url).searchParams; + const {filters, reverse, sortKey} = getFiltersFromParam(searchParams); + + for (const section of sections || []) { + if (section._type === 'collectionProductGridSection') { + const paginationVariables = getPaginationVariables(request, { + pageBy: section.productsPerPage || 8, + }); + + promise = storefront.query(COLLECTION_PRODUCT_GRID_QUERY, { + variables: { + ...paginationVariables, + country: storefront.i18n.country, + filters, + id: collectionId, + language: storefront.i18n.language, + reverse, + sortKey, + }, + }); + } + } + + return promise || null; +} diff --git a/templates/hydrogen-theme/app/lib/sectionRelsolver.ts b/templates/hydrogen-theme/app/lib/sectionRelsolver.ts index 0fb5ccf..3d33faa 100644 --- a/templates/hydrogen-theme/app/lib/sectionRelsolver.ts +++ b/templates/hydrogen-theme/app/lib/sectionRelsolver.ts @@ -8,11 +8,23 @@ export const sections: { default: module.CarouselSection, })), ), + collectionBannerSection: lazy(() => + import('../components/sections/CollectionBannerSection').then((module) => ({ + default: module.CollectionBannerSection, + })), + ), collectionListSection: lazy(() => import('../components/sections/CollectionListSection').then((module) => ({ default: module.CollectionListSection, })), ), + collectionProductGridSection: lazy(() => + import('../components/sections/CollectionProductGridSection').then( + (module) => ({ + default: module.CollectionProductGridSection, + }), + ), + ), ctaSection: lazy(() => import('../components/sections/CtaSection').then((module) => ({ default: module.CtaSection, diff --git a/templates/hydrogen-theme/app/lib/shopifyCollection.ts b/templates/hydrogen-theme/app/lib/shopifyCollection.ts new file mode 100644 index 0000000..66f5345 --- /dev/null +++ b/templates/hydrogen-theme/app/lib/shopifyCollection.ts @@ -0,0 +1,141 @@ +import type { + ProductCollectionSortKeys, + ProductFilter, +} from '@shopify/hydrogen/storefront-api-types'; + +import type {ShopifyCollection} from '~/components/sections/CollectionProductGridSection'; + +import { + FILTER_URL_PREFIX, + type SortParam, +} from '~/components/collection/SortFilter'; + +import type {I18nLocale} from './type'; + +import {parseAsCurrency} from './utils'; + +export function getFiltersFromParam(searchParams: URLSearchParams) { + const {reverse, sortKey} = getSortValuesFromParam( + searchParams.get('sort') as SortParam, + ); + + const filters = [...searchParams.entries()].reduce( + (filters, [key, value]) => { + if (key.startsWith(FILTER_URL_PREFIX)) { + const filterKey = key.substring(FILTER_URL_PREFIX.length); + filters.push({ + [filterKey]: JSON.parse(value), + }); + } + return filters; + }, + [] as ProductFilter[], + ); + + return { + filters, + reverse, + sortKey, + }; +} + +export function getAppliedFilters({ + collection, + locale, + searchParams, +}: { + collection?: ShopifyCollection; + locale?: I18nLocale; + searchParams: URLSearchParams; +}) { + if (!locale || !collection) { + return []; + } + + const {filters} = getFiltersFromParam(searchParams); + + const allFilterValues = collection?.products.filters.flatMap( + (filter) => filter.values, + ); + + return filters + .map((filter) => { + const foundValue = allFilterValues?.find((value) => { + const valueInput = JSON.parse(value.input as string) as ProductFilter; + // special case for price, the user can enter something freeform (still a number, though) + // that may not make sense for the locale/currency. + // Basically just check if the price filter is applied at all. + if (valueInput.price && filter.price) { + return true; + } + return ( + // This comparison should be okay as long as we're not manipulating the input we + // get from the API before using it as a URL param. + JSON.stringify(valueInput) === JSON.stringify(filter) + ); + }); + if (!foundValue) { + // eslint-disable-next-line no-console + console.error('Could not find filter value for filter', filter); + return null; + } + + if (foundValue.id === 'filter.v.price') { + // Special case for price, we want to show the min and max values as the label. + const input = JSON.parse(foundValue.input as string) as ProductFilter; + const min = parseAsCurrency(input.price?.min ?? 0, locale); + const max = input.price?.max + ? parseAsCurrency(input.price.max, locale) + : ''; + const label = min && max ? `${min} - ${max}` : 'Price'; + + return { + filter, + label, + }; + } + return { + filter, + label: foundValue.label, + }; + }) + .filter((filter): filter is NonNullable => filter !== null); +} + +export function getSortValuesFromParam(sortParam: SortParam | null): { + reverse: boolean; + sortKey: ProductCollectionSortKeys; +} { + switch (sortParam) { + case 'price-high-low': + return { + reverse: true, + sortKey: 'PRICE', + }; + case 'price-low-high': + return { + reverse: false, + sortKey: 'PRICE', + }; + case 'best-selling': + return { + reverse: false, + sortKey: 'BEST_SELLING', + }; + case 'newest': + return { + reverse: true, + sortKey: 'CREATED', + }; + case 'featured': + return { + reverse: false, + sortKey: 'MANUAL', + }; + default: + return { + reverse: false, + sortKey: 'RELEVANCE', + }; + } +} diff --git a/templates/hydrogen-theme/app/qroq/queries.ts b/templates/hydrogen-theme/app/qroq/queries.ts index 99afd32..d136acd 100644 --- a/templates/hydrogen-theme/app/qroq/queries.ts +++ b/templates/hydrogen-theme/app/qroq/queries.ts @@ -7,9 +7,38 @@ import { MENU_FRAGMENT, SETTINGS_FRAGMENT, } from './fragments'; -import {PRODUCT_SECTIONS_FRAGMENT, SECTIONS_FRAGMENT} from './sections'; +import { + COLLECTION_SECTIONS_FRAGMENT, + PRODUCT_SECTIONS_FRAGMENT, + SECTIONS_FRAGMENT, +} from './sections'; import {THEME_CONTENT_FRAGMENT} from './themeContent'; +/* +|-------------------------------------------------------------------------- +| Template Queries +|-------------------------------------------------------------------------- +*/ +export const DEFAULT_PRODUCT_TEMPLATE = q('*') + .filter("_type == 'productTemplate' && default == true") + .grab({ + _type: q.literal('productTemplate'), + name: q.string().nullable(), + sections: PRODUCT_SECTIONS_FRAGMENT, + }) + .slice(0) + .nullable(); + +export const DEFAULT_COLLECTION_TEMPLATE = q('*') + .filter("_type == 'collectionTemplate' && default == true") + .grab({ + _type: q.literal('collectionTemplate'), + name: q.string().nullable(), + sections: COLLECTION_SECTIONS_FRAGMENT, + }) + .slice(0) + .nullable(); + /* |-------------------------------------------------------------------------- | Page Query @@ -27,7 +56,7 @@ export const PAGE_QUERY = q('*') `, ) .grab({ - _type: q.literal('page'), + _type: q.literal('page').or(q.literal('home')), sections: SECTIONS_FRAGMENT, }) .slice(0) @@ -38,19 +67,44 @@ export const PAGE_QUERY = q('*') | Product Query |-------------------------------------------------------------------------- */ -export const PRODUCT_QUERY = q('*') - .filter(`_type == "product" && store.slug.current == $productHandle`) - .grab({ - _type: q.literal('product'), - store: q('store').grab({ - gid: q.string(), - }), - template: q('template').deref().grab({ - sections: PRODUCT_SECTIONS_FRAGMENT, - }), - }) - .slice(0) - .nullable(); +export const PRODUCT_QUERY = q('').grab({ + _type: ['"product"', q.literal('product')], + defaultProductTemplate: DEFAULT_PRODUCT_TEMPLATE, + product: q('*') + .filter(`_type == "product" && store.slug.current == $productHandle`) + .grab({ + store: q('store').grab({ + gid: q.string(), + }), + template: q('template').deref().grab({ + sections: PRODUCT_SECTIONS_FRAGMENT, + }), + }) + .slice(0) + .nullable(), +}); + +/* +|-------------------------------------------------------------------------- +| Collection Query +|-------------------------------------------------------------------------- +*/ +export const COLLECTION_QUERY = q('').grab({ + _type: ['"collection"', q.literal('collection')], + collection: q('*') + .filter(`_type == "collection" && store.slug.current == $collectionHandle`) + .grab({ + store: q('store').grab({ + gid: q.string(), + }), + template: q('template').deref().grab({ + sections: COLLECTION_SECTIONS_FRAGMENT, + }), + }) + .slice(0) + .nullable(), + defaultCollectionTemplate: DEFAULT_COLLECTION_TEMPLATE, +}); /* |-------------------------------------------------------------------------- @@ -74,21 +128,6 @@ export const DEFAULT_COLOR_SCHEME_QUERY = q('*') .slice(0) .nullable(); -export const DEFAULT_PRODUCT_TEMPLATE = q('*') - .filter("_type == 'productTemplate' && default == true") - .grab({ - _type: q.literal('productTemplate'), - name: q.string().nullable(), - sections: PRODUCT_SECTIONS_FRAGMENT, - }) - .slice(0) - .nullable(); - -export const DEFAULT_COLLECTION_TEMPLATE = q('*') - .filter("_type == 'collectionTemplate' && default == true") - .slice(0) - .nullable(); - export const SETTINGS_QUERY = q('*') .filter("_type == 'settings'") .grab(SETTINGS_FRAGMENT) @@ -129,9 +168,7 @@ export const THEME_CONTENT_QUERY = q('*') export const ROOT_QUERY = q('') .grab({ - defaultCollectionTemplate: DEFAULT_COLLECTION_TEMPLATE, defaultColorScheme: DEFAULT_COLOR_SCHEME_QUERY, - defaultProductTemplate: DEFAULT_PRODUCT_TEMPLATE, fonts: FONTS_QUERY, footer: FOOTER_QUERY, header: HEADER_QUERY, diff --git a/templates/hydrogen-theme/app/qroq/sections.ts b/templates/hydrogen-theme/app/qroq/sections.ts index 2dd01e8..c38721c 100644 --- a/templates/hydrogen-theme/app/qroq/sections.ts +++ b/templates/hydrogen-theme/app/qroq/sections.ts @@ -217,6 +217,35 @@ export const RICHTEXT_SECTION_FRAGMENT = { settings: SECTION_SETTINGS_FRAGMENT, } satisfies Selection; +/* +|-------------------------------------------------------------------------- +| Collection Banner Section +|-------------------------------------------------------------------------- +*/ +export const COLLECTION_BANNER_SECTION_FRAGMENT = { + _key: q.string().nullable(), + _type: q.literal('collectionBannerSection'), + settings: SECTION_SETTINGS_FRAGMENT, + showDescription: q.boolean().nullable(), + showImage: q.boolean().nullable(), +} satisfies Selection; + +/* +|-------------------------------------------------------------------------- +| Collection Banner Section +|-------------------------------------------------------------------------- +*/ +export const COLLECTION_PRODUCT_GRID_SECTION_FRAGMENT = { + _key: q.string().nullable(), + _type: q.literal('collectionProductGridSection'), + desktopColumns: q.number().nullable(), + enableFiltering: q.boolean().nullable(), + enableSorting: q.boolean().nullable(), + mobileColumns: q.number().nullable(), + productsPerPage: q.number().nullable(), + settings: SECTION_SETTINGS_FRAGMENT, +} satisfies Selection; + /* |-------------------------------------------------------------------------- | List of sections @@ -232,15 +261,15 @@ export const SECTIONS_LIST_SELECTION = { "_type == 'richtextSection'": RICHTEXT_SECTION_FRAGMENT, }; -/* -|-------------------------------------------------------------------------- -| Sections Fragment -|-------------------------------------------------------------------------- -*/ export const SECTIONS_FRAGMENT = q('sections[]', {isArray: true}) .select(SECTIONS_LIST_SELECTION) .nullable(); +/* +|-------------------------------------------------------------------------- +| Product Sections Fragment +|-------------------------------------------------------------------------- +*/ export const PRODUCT_SECTIONS_FRAGMENT = q('sections[]', {isArray: true}) .select({ "_type == 'productInformationSection'": @@ -249,3 +278,17 @@ export const PRODUCT_SECTIONS_FRAGMENT = q('sections[]', {isArray: true}) ...SECTIONS_LIST_SELECTION, }) .nullable(); + +/* +|-------------------------------------------------------------------------- +| Collection Sections Fragment +|-------------------------------------------------------------------------- +*/ +export const COLLECTION_SECTIONS_FRAGMENT = q('sections[]', {isArray: true}) + .select({ + "_type == 'collectionBannerSection'": COLLECTION_BANNER_SECTION_FRAGMENT, + "_type == 'collectionProductGridSection'": + COLLECTION_PRODUCT_GRID_SECTION_FRAGMENT, + ...SECTIONS_LIST_SELECTION, + }) + .nullable(); diff --git a/templates/hydrogen-theme/app/routes/($locale).$.tsx b/templates/hydrogen-theme/app/routes/($locale).$.tsx index f1ed8ef..e6a9193 100644 --- a/templates/hydrogen-theme/app/routes/($locale).$.tsx +++ b/templates/hydrogen-theme/app/routes/($locale).$.tsx @@ -35,6 +35,7 @@ export async function loader({context, params, request}: LoaderFunctionArgs) { featuredProductPromise, } = resolveShopifyPromises({ document: page, + request, storefront, }); diff --git a/templates/hydrogen-theme/app/routes/($locale).collections.$collectionHandle.tsx b/templates/hydrogen-theme/app/routes/($locale).collections.$collectionHandle.tsx index 8101fde..bab1fa3 100644 --- a/templates/hydrogen-theme/app/routes/($locale).collections.$collectionHandle.tsx +++ b/templates/hydrogen-theme/app/routes/($locale).collections.$collectionHandle.tsx @@ -1,260 +1,90 @@ -import type { - Filter, - ProductCollectionSortKeys, - ProductFilter, -} from '@shopify/hydrogen/storefront-api-types'; import type {LoaderFunctionArgs} from '@shopify/remix-oxygen'; -import type {ProductCardFragment} from 'storefrontapi.generated'; +import type {CollectionDetailsQuery} from 'storefrontapi.generated'; -import {useLoaderData, useNavigate} from '@remix-run/react'; -import { - Image, - Pagination, - flattenConnection, - getPaginationVariables, -} from '@shopify/hydrogen'; -import {json} from '@shopify/remix-oxygen'; -import {useEffect} from 'react'; +import {useLoaderData} from '@remix-run/react'; +import {defer} from '@shopify/remix-oxygen'; +import {DEFAULT_LOCALE} from 'countries'; import invariant from 'tiny-invariant'; -import type {SortParam} from '~/components/collection/SortFilter'; - -import { - FILTER_URL_PREFIX, - SortFilter, -} from '~/components/collection/SortFilter'; -import {ProductCardGrid} from '~/components/product/ProductCardGrid'; +import {CmsSection} from '~/components/CmsSection'; import {COLLECTION_QUERY} from '~/graphql/queries'; -import {parseAsCurrency} from '~/lib/utils'; +import {useSanityData} from '~/hooks/useSanityData'; +import {resolveShopifyPromises} from '~/lib/resolveShopifyPromises'; +import {sanityPreviewPayload} from '~/lib/sanity/sanity.payload.server'; +import {COLLECTION_QUERY as CMS_COLLECTION_QUERY} from '~/qroq/queries'; export async function loader({context, params, request}: LoaderFunctionArgs) { - const paginationVariables = getPaginationVariables(request, { - pageBy: 8, - }); const {collectionHandle} = params; - const {locale, storefront} = context; + const {locale, sanity, storefront} = context; + const language = locale?.language.toLowerCase(); invariant(collectionHandle, 'Missing collectionHandle param'); - const searchParams = new URL(request.url).searchParams; - - const {reverse, sortKey} = getSortValuesFromParam( - searchParams.get('sort') as SortParam, - ); - const filters = [...searchParams.entries()].reduce( - (filters, [key, value]) => { - if (key.startsWith(FILTER_URL_PREFIX)) { - const filterKey = key.substring(FILTER_URL_PREFIX.length); - filters.push({ - [filterKey]: JSON.parse(value), - }); - } - return filters; - }, - [] as ProductFilter[], - ); - - const {collection, collections} = await storefront.query(COLLECTION_QUERY, { - variables: { - ...paginationVariables, - country: storefront.i18n.country, - filters, - handle: collectionHandle, - language: storefront.i18n.language, - reverse, - sortKey, - }, - }); - - if (!collection) { + const queryParams = { + collectionHandle, + defaultLanguage: DEFAULT_LOCALE.language.toLowerCase(), + language, + }; + + const collectionData = Promise.all([ + sanity.query({ + groqdQuery: CMS_COLLECTION_QUERY, + params: queryParams, + }), + storefront.query(COLLECTION_QUERY, { + variables: { + country: storefront.i18n.country, + handle: collectionHandle, + language: storefront.i18n.language, + }, + }), + ]); + + const [cmsCollection, {collection}] = await collectionData; + + if (!collection?.id || !cmsCollection) { throw new Response('collection', {status: 404}); } - const allFilterValues = collection.products.filters.flatMap( - (filter) => filter.values, - ); - - const appliedFilters = filters - .map((filter) => { - const foundValue = allFilterValues.find((value) => { - const valueInput = JSON.parse(value.input as string) as ProductFilter; - // special case for price, the user can enter something freeform (still a number, though) - // that may not make sense for the locale/currency. - // Basically just check if the price filter is applied at all. - if (valueInput.price && filter.price) { - return true; - } - return ( - // This comparison should be okay as long as we're not manipulating the input we - // get from the API before using it as a URL param. - JSON.stringify(valueInput) === JSON.stringify(filter) - ); - }); - if (!foundValue) { - // eslint-disable-next-line no-console - console.error('Could not find filter value for filter', filter); - return null; - } - - if (foundValue.id === 'filter.v.price') { - // Special case for price, we want to show the min and max values as the label. - const input = JSON.parse(foundValue.input as string) as ProductFilter; - const min = parseAsCurrency(input.price?.min ?? 0, locale); - const max = input.price?.max - ? parseAsCurrency(input.price.max, locale) - : ''; - const label = min && max ? `${min} - ${max}` : 'Price'; - - return { - filter, - label, - }; - } - return { - filter, - label: foundValue.label, - }; - }) - .filter((filter): filter is NonNullable => filter !== null); + const { + collectionListPromise, + collectionProductGridPromise, + featuredCollectionPromise, + featuredProductPromise, + } = resolveShopifyPromises({ + document: cmsCollection, + request, + storefront, + }); - return json({ - appliedFilters, + return defer({ + cmsCollection, collection, - collections: flattenConnection(collections), + collectionListPromise, + collectionProductGridPromise, + featuredCollectionPromise, + featuredProductPromise, + ...sanityPreviewPayload({ + context, + params: queryParams, + query: CMS_COLLECTION_QUERY.query, + }), }); } export default function Collection() { - const {appliedFilters, collection, collections} = - useLoaderData(); - const products = collection.products.nodes.length - ? flattenConnection(collection.products) - : []; - - return ( - <> - {collection.image && ( -
    -
    - -
    -
    -

    {collection.title}

    -
    -
    -
    -
    - )} -
    - - - {({ - NextLink, - PreviousLink, - hasNextPage, - isLoading, - nextPageUrl, - nodes, - state, - }) => ( - <> -
    - - {isLoading ? 'Loading...' : 'Load previous'} - -
    - -
    - - {isLoading ? 'Loading...' : 'Load more products'} - -
    - - )} -
    -
    -
    - - ); -} - -function ProductsLoadedOnScroll({ - hasNextPage, - inView, - nextPageUrl, - nodes, - state, -}: { - hasNextPage: boolean; - inView: boolean; - nextPageUrl: string; - nodes: ProductCardFragment[]; - state: unknown; -}) { - const navigate = useNavigate(); - - useEffect(() => { - if (inView && hasNextPage) { - navigate(nextPageUrl, { - preventScrollReset: true, - replace: true, - state, - }); - } - }, [inView, navigate, state, nextPageUrl, hasNextPage]); - - return ; -} - -function getSortValuesFromParam(sortParam: SortParam | null): { - reverse: boolean; - sortKey: ProductCollectionSortKeys; -} { - switch (sortParam) { - case 'price-high-low': - return { - reverse: true, - sortKey: 'PRICE', - }; - case 'price-low-high': - return { - reverse: false, - sortKey: 'PRICE', - }; - case 'best-selling': - return { - reverse: false, - sortKey: 'BEST_SELLING', - }; - case 'newest': - return { - reverse: true, - sortKey: 'CREATED', - }; - case 'featured': - return { - reverse: false, - sortKey: 'MANUAL', - }; - default: - return { - reverse: false, - sortKey: 'RELEVANCE', - }; - } + const {cmsCollection} = useLoaderData(); + const {data, encodeDataAttribute} = useSanityData(cmsCollection); + const template = + data?.collection?.template || data?.defaultCollectionTemplate; + + return template?.sections && template.sections.length > 0 + ? template.sections.map((section) => ( + + )) + : null; } diff --git a/templates/hydrogen-theme/app/routes/($locale).products.$productHandle.tsx b/templates/hydrogen-theme/app/routes/($locale).products.$productHandle.tsx index 4c25696..4131137 100644 --- a/templates/hydrogen-theme/app/routes/($locale).products.$productHandle.tsx +++ b/templates/hydrogen-theme/app/routes/($locale).products.$productHandle.tsx @@ -71,6 +71,7 @@ export async function loader({context, params, request}: LoaderFunctionArgs) { relatedProductsPromise, } = resolveShopifyPromises({ document: cmsProduct, + request, storefront, }); @@ -92,10 +93,8 @@ export async function loader({context, params, request}: LoaderFunctionArgs) { export default function Product() { const {cmsProduct} = useLoaderData(); - const {data: rootData} = useSanityRoot(); const {data, encodeDataAttribute} = useSanityData(cmsProduct); - - const template = data?.template || rootData?.defaultProductTemplate; + const template = data?.product?.template || data?.defaultProductTemplate; return template?.sections && template.sections.length > 0 ? template.sections.map((section) => ( diff --git a/templates/hydrogen-theme/app/routes/_index.tsx b/templates/hydrogen-theme/app/routes/_index.tsx index 8a8223d..cbb252e 100644 --- a/templates/hydrogen-theme/app/routes/_index.tsx +++ b/templates/hydrogen-theme/app/routes/_index.tsx @@ -9,7 +9,7 @@ import {PAGE_QUERY} from '~/qroq/queries'; import PageRoute from './($locale).$'; -export async function loader({context}: LoaderFunctionArgs) { +export async function loader({context, request}: LoaderFunctionArgs) { const {locale, sanity, storefront} = context; const language = locale?.language.toLowerCase(); const queryParams = { @@ -29,6 +29,7 @@ export async function loader({context}: LoaderFunctionArgs) { featuredProductPromise, } = resolveShopifyPromises({ document: page, + request, storefront, }); diff --git a/templates/hydrogen-theme/storefrontapi.generated.d.ts b/templates/hydrogen-theme/storefrontapi.generated.d.ts index 748fee7..5a8896e 100644 --- a/templates/hydrogen-theme/storefrontapi.generated.d.ts +++ b/templates/hydrogen-theme/storefrontapi.generated.d.ts @@ -519,6 +519,25 @@ export type CollectionDetailsQueryVariables = StorefrontAPI.Exact<{ handle: StorefrontAPI.Scalars['String']['input']; country?: StorefrontAPI.InputMaybe; language?: StorefrontAPI.InputMaybe; +}>; + +export type CollectionDetailsQuery = { + collection?: StorefrontAPI.Maybe< + Pick< + StorefrontAPI.Collection, + 'id' | 'handle' | 'title' | 'description' + > & { + image?: StorefrontAPI.Maybe< + Pick + >; + } + >; +}; + +export type CollectionProductGridQueryVariables = StorefrontAPI.Exact<{ + id: StorefrontAPI.Scalars['ID']['input']; + country?: StorefrontAPI.InputMaybe; + language?: StorefrontAPI.InputMaybe; filters?: StorefrontAPI.InputMaybe< Array | StorefrontAPI.ProductFilter >; @@ -534,15 +553,9 @@ export type CollectionDetailsQueryVariables = StorefrontAPI.Exact<{ >; }>; -export type CollectionDetailsQuery = { +export type CollectionProductGridQuery = { collection?: StorefrontAPI.Maybe< - Pick< - StorefrontAPI.Collection, - 'id' | 'handle' | 'title' | 'description' - > & { - image?: StorefrontAPI.Maybe< - Pick - >; + Pick & { products: { filters: Array< Pick & { @@ -591,9 +604,6 @@ export type CollectionDetailsQuery = { }; } >; - collections: { - edges: Array<{node: Pick}>; - }; }; export type FeaturedCollectionQueryVariables = StorefrontAPI.Exact<{ @@ -673,10 +683,14 @@ interface GeneratedQueryTypes { return: CollectionsQuery; variables: CollectionsQueryVariables; }; - '#graphql\n query CollectionDetails(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n $filters: [ProductFilter!]\n $sortKey: ProductCollectionSortKeys!\n $reverse: Boolean\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n image {\n id\n url\n width\n height\n altText\n }\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor,\n filters: $filters,\n sortKey: $sortKey,\n reverse: $reverse\n ) {\n filters {\n id\n label\n type\n values {\n id\n label\n count\n input\n }\n }\n nodes {\n ...ProductCard\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n collections(first: 100) {\n edges {\n node {\n title\n handle\n }\n }\n }\n }\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n variants(first: 1) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n }\n }\n }\n\n': { + '#graphql\n query CollectionDetails(\n $handle: String!\n $country: CountryCode\n $language: LanguageCode\n ) @inContext(country: $country, language: $language) {\n collection(handle: $handle) {\n id\n handle\n title\n description\n image {\n id\n url\n width\n height\n altText\n }\n }\n }\n': { return: CollectionDetailsQuery; variables: CollectionDetailsQueryVariables; }; + '#graphql\n query CollectionProductGrid(\n $id: ID!\n $country: CountryCode\n $language: LanguageCode\n $filters: [ProductFilter!]\n $sortKey: ProductCollectionSortKeys!\n $reverse: Boolean\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n collection(id: $id) {\n id\n handle\n products(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor,\n filters: $filters,\n sortKey: $sortKey,\n reverse: $reverse\n ) {\n filters {\n id\n label\n type\n values {\n id\n label\n count\n input\n }\n }\n nodes {\n ...ProductCard\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n endCursor\n startCursor\n }\n }\n }\n }\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n variants(first: 1) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n }\n }\n }\n\n': { + return: CollectionProductGridQuery; + variables: CollectionProductGridQueryVariables; + }; '#graphql\n query FeaturedCollection(\n $id: ID!\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n ) @inContext(country: $country, language: $language) {\n collection(id: $id) {\n id\n handle\n title\n description\n image {\n id\n url\n width\n height\n altText\n }\n products(\n first: $first,\n ) {\n nodes {\n ...ProductCard\n }\n }\n }\n }\n #graphql\n fragment ProductCard on Product {\n id\n title\n publishedAt\n handle\n vendor\n variants(first: 1) {\n nodes {\n id\n availableForSale\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n compareAtPrice {\n amount\n currencyCode\n }\n selectedOptions {\n name\n value\n }\n product {\n handle\n title\n }\n }\n }\n }\n\n': { return: FeaturedCollectionQuery; variables: FeaturedCollectionQueryVariables; diff --git a/templates/hydrogen-theme/studio/schemas/documents/collectionTemplate.tsx b/templates/hydrogen-theme/studio/schemas/documents/collectionTemplate.tsx index 62cf6ae..672b37a 100644 --- a/templates/hydrogen-theme/studio/schemas/documents/collectionTemplate.tsx +++ b/templates/hydrogen-theme/studio/schemas/documents/collectionTemplate.tsx @@ -23,6 +23,10 @@ export default defineType({ ), initialValue: false, }), + defineField({ + name: 'sections', + type: 'collectionSections', + }), ], preview: { select: { diff --git a/templates/hydrogen-theme/studio/schemas/index.ts b/templates/hydrogen-theme/studio/schemas/index.ts index 1851e99..cedacd5 100644 --- a/templates/hydrogen-theme/studio/schemas/index.ts +++ b/templates/hydrogen-theme/studio/schemas/index.ts @@ -10,7 +10,10 @@ import home from './singletons/home'; import collection from './documents/collection'; import product from './documents/product'; import blogPost from './documents/blogPost'; -import sectionsList, {productSections} from './objects/global/sectionsList'; +import sectionsList, { + productSections, + collectionSections, +} from './objects/global/sectionsList'; import seo from './objects/global/seo'; import sectionSettings from './objects/global/sectionSettings'; import headerNavigation from './objects/global/headerNavigation'; @@ -40,6 +43,8 @@ import richtextSection from './objects/sections/richtextSection'; import richtext from './objects/global/richtext'; import productTemplate from './documents/productTemplate'; import collectionTemplate from './documents/collectionTemplate'; +import collectionBanner from './objects/sections/collectionBanner'; +import collectionProductGrid from './objects/sections/collectionProductGrid'; const singletons = [home, header, footer, settings, themeContent]; const documents = [ @@ -62,12 +67,15 @@ const sections = [ relatedProductsSection, carouselSection, richtextSection, + collectionBanner, + collectionProductGrid, ]; const footers = [socialLinksOnly]; const objects = [ footersList, sectionsList, productSections, + collectionSections, productRichtext, seo, sectionSettings, diff --git a/templates/hydrogen-theme/studio/schemas/objects/global/sectionsList.ts b/templates/hydrogen-theme/studio/schemas/objects/global/sectionsList.ts index df5f49f..d0f6d0e 100644 --- a/templates/hydrogen-theme/studio/schemas/objects/global/sectionsList.ts +++ b/templates/hydrogen-theme/studio/schemas/objects/global/sectionsList.ts @@ -36,6 +36,16 @@ const pdpSections = [ ...globalSections, ]; +const collectionSectionsList = [ + { + type: 'collectionBannerSection', + }, + { + type: 'collectionProductGridSection', + }, + ...globalSections, +]; + export default defineField({ title: 'Sections', name: 'sections', @@ -59,3 +69,15 @@ export const productSections = defineField({ SectionsListInput({type: 'section', ...props}), }, }); + +export const collectionSections = defineField({ + title: 'Sections', + name: 'collectionSections', + type: 'array', + group: 'pagebuilder', + of: collectionSectionsList, + components: { + input: (props: ArrayOfObjectsInputProps) => + SectionsListInput({type: 'section', ...props}), + }, +}); diff --git a/templates/hydrogen-theme/studio/schemas/objects/sections/collectionBanner.tsx b/templates/hydrogen-theme/studio/schemas/objects/sections/collectionBanner.tsx new file mode 100644 index 0000000..50c34f8 --- /dev/null +++ b/templates/hydrogen-theme/studio/schemas/objects/sections/collectionBanner.tsx @@ -0,0 +1,33 @@ +import {defineField} from 'sanity'; +import {Image} from 'lucide-react'; + +export default defineField({ + name: 'collectionBannerSection', + title: 'Collection Banner', + type: 'object', + fields: [ + defineField({ + name: 'showImage', + title: 'Show collection image', + description: 'For best results, use an image with a 16:9 aspect ratio.', + type: 'boolean', + }), + defineField({ + name: 'showDescription', + title: 'Show collection description', + type: 'boolean', + }), + defineField({ + type: 'sectionSettings', + name: 'settings', + }), + ], + preview: { + prepare() { + return { + title: 'Collection Banner', + media: () => , + }; + }, + }, +}); diff --git a/templates/hydrogen-theme/studio/schemas/objects/sections/collectionProductGrid.tsx b/templates/hydrogen-theme/studio/schemas/objects/sections/collectionProductGrid.tsx new file mode 100644 index 0000000..57c6521 --- /dev/null +++ b/templates/hydrogen-theme/studio/schemas/objects/sections/collectionProductGrid.tsx @@ -0,0 +1,67 @@ +import {defineField} from 'sanity'; +import {LayoutGrid} from 'lucide-react'; + +export default defineField({ + name: 'collectionProductGridSection', + title: 'Product Grid', + type: 'object', + fields: [ + defineField({ + name: 'productsPerPage', + type: 'rangeSlider', + options: { + min: 8, + max: 14, + step: 4, + }, + }), + defineField({ + name: 'desktopColumns', + title: 'Number of columns on desktop', + type: 'rangeSlider', + options: { + min: 1, + max: 5, + }, + validation: (Rule: any) => Rule.required().min(1).max(5), + }), + defineField({ + name: 'mobileColumns', + title: 'Number of columns on mobile', + type: 'rangeSlider', + options: { + min: 1, + max: 2, + }, + validation: (Rule: any) => Rule.required().min(1).max(2), + }), + defineField({ + name: 'enableFiltering', + description: 'Customize filters with the Search & Discovery Shopify app.', + type: 'boolean', + }), + defineField({ + name: 'enableSorting', + type: 'boolean', + }), + defineField({ + type: 'sectionSettings', + name: 'settings', + }), + ], + initialValue: { + productsPerPage: 8, + desktopColumns: 4, + mobileColumns: 2, + enableFiltering: true, + enableSorting: true, + }, + preview: { + prepare() { + return { + title: 'Collection Product Grid', + media: () => , + }; + }, + }, +}); diff --git a/templates/hydrogen-theme/studio/static/assets/collectionBannerSection.png b/templates/hydrogen-theme/studio/static/assets/collectionBannerSection.png new file mode 100644 index 0000000000000000000000000000000000000000..2752d2cf0651e71c11c9cd9156e3c5811f7c2549 GIT binary patch literal 14189 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 ODFeN;CyR6)!v7CDKcF=L literal 0 HcmV?d00001 diff --git a/templates/hydrogen-theme/studio/static/assets/collectionProductGridSection.png b/templates/hydrogen-theme/studio/static/assets/collectionProductGridSection.png new file mode 100644 index 0000000000000000000000000000000000000000..dca924247d43e1290ef47ed5809b695d4ce46241 GIT binary patch literal 30122 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 literal 0 HcmV?d00001