From c9835d318b79df36dabb30bdd49117f6b19876de Mon Sep 17 00:00:00 2001 From: Daniel Almaguer Date: Thu, 11 Apr 2024 15:17:11 -0500 Subject: [PATCH] refactor(core): homepage fragment colocation --- .../app/[locale]/(default)/compare/page.tsx | 75 +++++++++++++-- apps/core/app/[locale]/(default)/page.tsx | 51 ++++++++-- .../[slug]/_components/related-products.tsx | 33 +++++-- .../(default)/product/[slug]/page.tsx | 40 +++++++- apps/core/app/[locale]/not-found.tsx | 14 +-- .../client/queries/get-newest-products.ts | 48 ---------- .../queries/get-product-search-results.ts | 1 - apps/core/components/forbidden/index.tsx | 33 ------- apps/core/components/pricing/index.tsx | 53 +++++++++-- .../product-card-carousel/index.tsx | 19 +++- .../product-card-carousel/pagination.tsx | 13 +-- .../components/product-card/cart/fragment.ts | 16 ++++ .../product-card/{cart.tsx => cart/index.tsx} | 22 +++-- apps/core/components/product-card/index.tsx | 95 ++++++++----------- apps/core/components/quick-search/index.tsx | 2 +- 15 files changed, 313 insertions(+), 202 deletions(-) delete mode 100644 apps/core/client/queries/get-newest-products.ts delete mode 100644 apps/core/components/forbidden/index.tsx create mode 100644 apps/core/components/product-card/cart/fragment.ts rename apps/core/components/product-card/{cart.tsx => cart/index.tsx} (79%) diff --git a/apps/core/app/[locale]/(default)/compare/page.tsx b/apps/core/app/[locale]/(default)/compare/page.tsx index cf720fe6a..9440dc9b1 100644 --- a/apps/core/app/[locale]/(default)/compare/page.tsx +++ b/apps/core/app/[locale]/(default)/compare/page.tsx @@ -1,13 +1,17 @@ +import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client'; import { Button } from '@bigcommerce/components/button'; import { Rating } from '@bigcommerce/components/rating'; import { NextIntlClientProvider } from 'next-intl'; import { getMessages, getTranslations } from 'next-intl/server'; import * as z from 'zod'; -import { getProducts } from '~/client/queries/get-products'; +import { getSessionCustomerId } from '~/auth'; +import { client } from '~/client'; +import { graphql } from '~/client/graphql'; +import { revalidate } from '~/client/revalidate-target'; import { BcImage } from '~/components/bc-image'; import { Link } from '~/components/link'; -import { Pricing } from '~/components/pricing'; +import { Pricing, PricingFragment } from '~/components/pricing'; import { SearchForm } from '~/components/search-form'; import { LocaleType } from '~/i18n'; import { cn } from '~/lib/utils'; @@ -37,6 +41,53 @@ const CompareParamsSchema = z.object({ .transform((value) => value?.map((id) => parseInt(id, 10))), }); +const ComparePageQuery = graphql( + ` + query ComparePage($entityIds: [Int!], $first: Int) { + site { + products(entityIds: $entityIds, first: $first) { + edges { + node { + entityId + name + path + brand { + name + } + defaultImage { + altText + url: urlTemplate + } + reviewSummary { + numberOfReviews + averageRating + } + productOptions(first: 3) { + edges { + node { + entityId + } + } + } + description + inventory { + aggregated { + availableToSell + } + } + availabilityV2 { + status + } + ...PricingFragment + } + } + } + } + } + `, + [PricingFragment], +); + export default async function Compare({ params: { locale }, searchParams, @@ -44,16 +95,28 @@ export default async function Compare({ searchParams: { [key: string]: string | string[] | undefined }; params: { locale: LocaleType }; }) { + const customerId = await getSessionCustomerId(); const t = await getTranslations({ locale, namespace: 'Compare' }); const messages = await getMessages({ locale }); const parsed = CompareParamsSchema.parse(searchParams); const productIds = parsed.ids?.filter((id) => !Number.isNaN(id)); - const products = await getProducts({ - productIds: productIds ?? [], - first: productIds?.length ? MAX_COMPARE_LIMIT : 0, + + const { data } = await client.fetch({ + document: ComparePageQuery, + variables: { + entityIds: productIds ?? [], + first: productIds?.length ? MAX_COMPARE_LIMIT : 0, + }, + customerId, + fetchOptions: customerId ? { cache: 'no-store' } : { next: { revalidate } }, }); + const products = removeEdgesAndNodes(data.site.products).map((product) => ({ + ...product, + productOptions: removeEdgesAndNodes(product.productOptions), + })); + if (!products.length) { return (
@@ -136,7 +199,7 @@ export default async function Compare({ {products.map((product) => ( {/* TODO: add translations */} - + ))} diff --git a/apps/core/app/[locale]/(default)/page.tsx b/apps/core/app/[locale]/(default)/page.tsx index c158af07f..e466c10ce 100644 --- a/apps/core/app/[locale]/(default)/page.tsx +++ b/apps/core/app/[locale]/(default)/page.tsx @@ -1,10 +1,16 @@ +import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client'; import { NextIntlClientProvider } from 'next-intl'; import { getMessages, getTranslations, unstable_setRequestLocale } from 'next-intl/server'; -import { getFeaturedProducts } from '~/client/queries/get-featured-products'; -import { getNewestProducts } from '~/client/queries/get-newest-products'; +import { getSessionCustomerId } from '~/auth'; +import { client } from '~/client'; +import { graphql } from '~/client/graphql'; +import { revalidate } from '~/client/revalidate-target'; import { Hero } from '~/components/hero'; -import { ProductCardCarousel } from '~/components/product-card-carousel'; +import { + ProductCardCarousel, + ProductCardCarouselFragment, +} from '~/components/product-card-carousel'; import { LocaleType } from '~/i18n'; interface Props { @@ -13,15 +19,46 @@ interface Props { }; } +const HomePageQuery = graphql( + ` + query HomePageQuery { + site { + newestProducts(first: 12) { + edges { + node { + ...ProductCardCarouselFragment + } + } + } + featuredProducts(first: 12) { + edges { + node { + ...ProductCardCarouselFragment + } + } + } + } + } + `, + [ProductCardCarouselFragment], +); + export default async function Home({ params: { locale } }: Props) { + const customerId = await getSessionCustomerId(); + unstable_setRequestLocale(locale); const t = await getTranslations({ locale, namespace: 'Home' }); const messages = await getMessages({ locale }); - const [newestProducts, featuredProducts] = await Promise.all([ - getNewestProducts(), - getFeaturedProducts(), - ]); + + const { data } = await client.fetch({ + document: HomePageQuery, + customerId, + fetchOptions: customerId ? { cache: 'no-store' } : { next: { revalidate } }, + }); + + const featuredProducts = removeEdgesAndNodes(data.site.featuredProducts); + const newestProducts = removeEdgesAndNodes(data.site.newestProducts); return ( <> diff --git a/apps/core/app/[locale]/(default)/product/[slug]/_components/related-products.tsx b/apps/core/app/[locale]/(default)/product/[slug]/_components/related-products.tsx index be8fe56f8..b265e0db0 100644 --- a/apps/core/app/[locale]/(default)/product/[slug]/_components/related-products.tsx +++ b/apps/core/app/[locale]/(default)/product/[slug]/_components/related-products.tsx @@ -1,17 +1,38 @@ +import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client'; import { NextIntlClientProvider } from 'next-intl'; import { getLocale, getMessages, getTranslations } from 'next-intl/server'; -import { getRelatedProducts } from '~/client/queries/get-related-products'; -import { ProductCardCarousel } from '~/components/product-card-carousel'; +import { graphql, ResultOf } from '~/client/graphql'; +import { + ProductCardCarousel, + ProductCardCarouselFragment, +} from '~/components/product-card-carousel'; -export const RelatedProducts = async ({ productId }: { productId: number }) => { +export const RelatedProductsFragment = graphql( + ` + fragment RelatedProductsFragment on Product { + relatedProducts(first: 12) { + edges { + node { + ...ProductCardCarouselFragment + } + } + } + } + `, + [ProductCardCarouselFragment], +); + +interface Props { + data: ResultOf; +} + +export const RelatedProducts = async ({ data }: Props) => { const t = await getTranslations('Product'); const locale = await getLocale(); const messages = await getMessages({ locale }); - const relatedProducts = await getRelatedProducts({ - productId, - }); + const relatedProducts = removeEdgesAndNodes(data.relatedProducts); return ( diff --git a/apps/core/app/[locale]/(default)/product/[slug]/page.tsx b/apps/core/app/[locale]/(default)/product/[slug]/page.tsx index 3ed4a6a1a..0e04618ae 100644 --- a/apps/core/app/[locale]/(default)/product/[slug]/page.tsx +++ b/apps/core/app/[locale]/(default)/product/[slug]/page.tsx @@ -4,14 +4,18 @@ import { NextIntlClientProvider } from 'next-intl'; import { getMessages, getTranslations, unstable_setRequestLocale } from 'next-intl/server'; import { Suspense } from 'react'; +import { getSessionCustomerId } from '~/auth'; +import { client } from '~/client'; +import { graphql } from '~/client/graphql'; import { getProduct } from '~/client/queries/get-product'; +import { revalidate } from '~/client/revalidate-target'; import { LocaleType } from '~/i18n'; import { BreadCrumbs } from './_components/breadcrumbs'; import { Description } from './_components/description'; import { Details } from './_components/details'; import { Gallery } from './_components/gallery'; -import { RelatedProducts } from './_components/related-products'; +import { RelatedProducts, RelatedProductsFragment } from './_components/related-products'; import { Reviews } from './_components/reviews'; import { Warranty } from './_components/warranty'; @@ -48,7 +52,22 @@ export async function generateMetadata({ params }: ProductPageProps): Promise !Number.isNaN(option.optionEntityId) && !Number.isNaN(option.valueEntityId), ); - const product = await getProduct(productId, optionValueIds); + // TODO: Here we are temporarily fetching the same product twice + // This is part of the ongoing effort of migrating to fragment collocation + const [product, { data }] = await Promise.all([ + getProduct(productId, optionValueIds), + + client.fetch({ + document: ProductPageQuery, + variables: { entityId: productId, optionValueIds }, + customerId, + fetchOptions: customerId ? { cache: 'no-store' } : { next: { revalidate } }, + }), + ]); if (!product) { return notFound(); } + if (!data.site.product) { + return notFound(); + } + return ( <> @@ -92,7 +126,7 @@ export default async function Product({ params, searchParams }: ProductPageProps
- + ); diff --git a/apps/core/app/[locale]/not-found.tsx b/apps/core/app/[locale]/not-found.tsx index 4890eabe4..3bf3760a1 100644 --- a/apps/core/app/[locale]/not-found.tsx +++ b/apps/core/app/[locale]/not-found.tsx @@ -4,13 +4,12 @@ import { NextIntlClientProvider } from 'next-intl'; import { getLocale, getMessages, getTranslations } from 'next-intl/server'; import { client } from '~/client'; -import { PRODUCT_DETAILS_FRAGMENT } from '~/client/fragments/product-details'; import { graphql } from '~/client/graphql'; import { revalidate } from '~/client/revalidate-target'; import { Footer, FooterFragment } from '~/components/footer/footer'; import { Header, HeaderFragment } from '~/components/header'; import { CartLink } from '~/components/header/cart'; -import { ProductCard } from '~/components/product-card'; +import { ProductCard, ProductCardFragment } from '~/components/product-card'; import { SearchForm } from '~/components/search-form'; export const metadata = { @@ -26,14 +25,14 @@ const NotFoundQuery = graphql( featuredProducts(first: 4) { edges { node { - ...ProductDetails + ...ProductCardFragment } } } } } `, - [HeaderFragment, FooterFragment, PRODUCT_DETAILS_FRAGMENT], + [HeaderFragment, FooterFragment, ProductCardFragment], ); export default async function NotFound() { @@ -46,12 +45,7 @@ export default async function NotFound() { fetchOptions: { next: { revalidate } }, }); - const featuredProducts = removeEdgesAndNodes(data.site.featuredProducts).map( - (featuredProduct) => ({ - ...featuredProduct, - productOptions: removeEdgesAndNodes(featuredProduct.productOptions), - }), - ); + const featuredProducts = removeEdgesAndNodes(data.site.featuredProducts); return ( <> diff --git a/apps/core/client/queries/get-newest-products.ts b/apps/core/client/queries/get-newest-products.ts deleted file mode 100644 index 9d4003a9d..000000000 --- a/apps/core/client/queries/get-newest-products.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client'; -import { cache } from 'react'; - -import { getSessionCustomerId } from '~/auth'; - -import { client } from '..'; -import { PRODUCT_DETAILS_FRAGMENT } from '../fragments/product-details'; -import { graphql } from '../graphql'; -import { revalidate } from '../revalidate-target'; - -const GET_NEWEST_PRODUCTS_QUERY = graphql( - ` - query getNewestProducts($first: Int) { - site { - newestProducts(first: $first) { - edges { - node { - ...ProductDetails - } - } - } - } - } - `, - [PRODUCT_DETAILS_FRAGMENT], -); - -interface Options { - first?: number; -} - -export const getNewestProducts = cache(async ({ first = 12 }: Options = {}) => { - const customerId = await getSessionCustomerId(); - - const response = await client.fetch({ - document: GET_NEWEST_PRODUCTS_QUERY, - variables: { first }, - customerId, - fetchOptions: customerId ? { cache: 'no-store' } : { next: { revalidate } }, - }); - - const { site } = response.data; - - return removeEdgesAndNodes(site.newestProducts).map((product) => ({ - ...product, - productOptions: removeEdgesAndNodes(product.productOptions), - })); -}); diff --git a/apps/core/client/queries/get-product-search-results.ts b/apps/core/client/queries/get-product-search-results.ts index 7c1cf0ce5..de08be811 100644 --- a/apps/core/client/queries/get-product-search-results.ts +++ b/apps/core/client/queries/get-product-search-results.ts @@ -184,7 +184,6 @@ export const getProductSearchResults = cache( const items = removeEdgesAndNodes(searchResults.products).map((product) => ({ ...product, - productOptions: removeEdgesAndNodes(product.productOptions), fetchOptions: { next: { revalidate } }, })); diff --git a/apps/core/components/forbidden/index.tsx b/apps/core/components/forbidden/index.tsx deleted file mode 100644 index b68ceaa96..000000000 --- a/apps/core/components/forbidden/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { getFeaturedProducts } from '~/client/queries/get-featured-products'; -import { ProductCard } from '~/components/product-card'; -import { SearchForm } from '~/components/search-form'; - -const FeaturedProducts = async () => { - const featuredProducts = await getFeaturedProducts(); - - return ( -
-

- Featured products -

-
- {featuredProducts.map((product) => ( - - ))} -
-
- ); -}; - -export const Forbidden = () => { - return ( -
-
-

There was a problem!

-

It looks like the page you requested can't be accessed.

-
- - -
- ); -}; diff --git a/apps/core/components/pricing/index.tsx b/apps/core/components/pricing/index.tsx index cd95db57c..ef410eae1 100644 --- a/apps/core/components/pricing/index.tsx +++ b/apps/core/components/pricing/index.tsx @@ -1,22 +1,59 @@ -import { Product } from '../product-card'; +import { graphql, ResultOf } from '~/client/graphql'; + +export const PricingFragment = graphql(` + fragment PricingFragment on Product { + prices { + price { + value + currencyCode + } + basePrice { + value + currencyCode + } + retailPrice { + value + currencyCode + } + salePrice { + value + currencyCode + } + priceRange { + min { + value + currencyCode + } + max { + value + currencyCode + } + } + } + } +`); + +interface Props { + data: ResultOf; +} + +export const Pricing = ({ data }: Props) => { + const { prices } = data; -export const Pricing = ({ prices }: { prices: Product['prices'] }) => { if (!prices) { return null; } const currencyFormatter = new Intl.NumberFormat('en-US', { style: 'currency', - currency: prices.price?.currencyCode, + currency: prices.price.currencyCode, }); - const showPriceRange = prices.priceRange?.min?.value !== prices.priceRange?.max?.value; + const showPriceRange = prices.priceRange.min.value !== prices.priceRange.max.value; return (

- {showPriceRange && - prices.priceRange?.min?.value !== undefined && - prices.priceRange.max?.value !== undefined ? ( + {showPriceRange ? ( <> {currencyFormatter.format(prices.priceRange.min.value)} -{' '} {currencyFormatter.format(prices.priceRange.max.value)} @@ -42,7 +79,7 @@ export const Pricing = ({ prices }: { prices: Product['prices'] }) => { <>Now: {currencyFormatter.format(prices.salePrice.value)} ) : ( - prices.price?.value && <>{currencyFormatter.format(prices.price.value)} + prices.price.value && <>{currencyFormatter.format(prices.price.value)} )} )} diff --git a/apps/core/components/product-card-carousel/index.tsx b/apps/core/components/product-card-carousel/index.tsx index 38ee8aced..9e3c9f30c 100644 --- a/apps/core/components/product-card-carousel/index.tsx +++ b/apps/core/components/product-card-carousel/index.tsx @@ -7,10 +7,23 @@ import { } from '@bigcommerce/components/carousel'; import { useId } from 'react'; -import { Product, ProductCard } from '../product-card'; +import { graphql, ResultOf } from '~/client/graphql'; + +import { ProductCard, ProductCardFragment } from '../product-card'; import { Pagination } from './pagination'; +export const ProductCardCarouselFragment = graphql( + ` + fragment ProductCardCarouselFragment on Product { + ...ProductCardFragment + } + `, + [ProductCardFragment], +); + +type Product = ResultOf; + export const ProductCardCarousel = ({ title, products, @@ -19,7 +32,7 @@ export const ProductCardCarousel = ({ showReviews = true, }: { title: string; - products: Array>; + products: Product[]; showCart?: boolean; showCompare?: boolean; showReviews?: boolean; @@ -30,7 +43,7 @@ export const ProductCardCarousel = ({ return null; } - const groupedProducts = products.reduce>>>((batches, _, index) => { + const groupedProducts = products.reduce((batches, _, index) => { if (index % 4 === 0) { batches.push([]); } diff --git a/apps/core/components/product-card-carousel/pagination.tsx b/apps/core/components/product-card-carousel/pagination.tsx index 9c8b819d3..65e799dd9 100644 --- a/apps/core/components/product-card-carousel/pagination.tsx +++ b/apps/core/components/product-card-carousel/pagination.tsx @@ -2,15 +2,12 @@ import { CarouselPagination, CarouselPaginationTab } from '@bigcommerce/components/carousel'; -import { Product } from '../product-card'; - -export const Pagination = ({ - groupedProducts, - id, -}: { - groupedProducts: Array>>; +interface Props { id: string; -}) => { + groupedProducts: unknown[]; +} + +export const Pagination = ({ groupedProducts, id }: Props) => { return ( {groupedProducts.map((_, index) => ( diff --git a/apps/core/components/product-card/cart/fragment.ts b/apps/core/components/product-card/cart/fragment.ts new file mode 100644 index 000000000..408f165ba --- /dev/null +++ b/apps/core/components/product-card/cart/fragment.ts @@ -0,0 +1,16 @@ +import { graphql, ResultOf } from '~/client/graphql'; + +export const CartFragment = graphql(` + fragment CartFragment on Product { + entityId + productOptions(first: 10) { + edges { + node { + entityId + } + } + } + } +`); + +export type CartFragmentResult = ResultOf; diff --git a/apps/core/components/product-card/cart.tsx b/apps/core/components/product-card/cart/index.tsx similarity index 79% rename from apps/core/components/product-card/cart.tsx rename to apps/core/components/product-card/cart/index.tsx index 4d31b0230..7e2241754 100644 --- a/apps/core/components/product-card/cart.tsx +++ b/apps/core/components/product-card/cart/index.tsx @@ -1,33 +1,35 @@ 'use client'; +import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client'; import { Button } from '@bigcommerce/components/button'; import { AlertCircle, Check } from 'lucide-react'; import { usePathname, useSearchParams } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { toast } from 'react-hot-toast'; -import { Link } from '../link'; +import { Link } from '../../link'; +import { addToCart } from '../_actions/add-to-cart'; +import { AddToCart } from '../add-to-cart'; -import { addToCart } from './_actions/add-to-cart'; -import { AddToCart } from './add-to-cart'; +import { type CartFragmentResult } from './fragment'; -import { Product } from '.'; +interface Props { + data: CartFragmentResult; +} -export const Cart = ({ product }: { product: Partial }) => { +export const Cart = ({ data: product }: Props) => { const pathname = usePathname(); const searchParams = useSearchParams(); const t = useTranslations('Product.ProductSheet'); - if (!product.entityId) { - return null; - } - const newSearchParams = new URLSearchParams(searchParams); newSearchParams.set('showQuickAdd', String(product.entityId)); - return Array.isArray(product.productOptions) && product.productOptions.length > 0 ? ( + const productOptions = removeEdgesAndNodes(product.productOptions); + + return Array.isArray(productOptions) && productOptions.length > 0 ? (