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 87%
rename from apps/core/components/product-card/cart.tsx
rename to apps/core/components/product-card/cart/index.tsx
index 4d31b0230..3389232e5 100644
--- a/apps/core/components/product-card/cart.tsx
+++ b/apps/core/components/product-card/cart/index.tsx
@@ -6,23 +6,22 @@ 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));
diff --git a/apps/core/components/product-card/index.tsx b/apps/core/components/product-card/index.tsx
index c9f5e0db3..685db0489 100644
--- a/apps/core/components/product-card/index.tsx
+++ b/apps/core/components/product-card/index.tsx
@@ -10,66 +10,44 @@ import { Rating } from '@bigcommerce/components/rating';
import { useTranslations } from 'next-intl';
import { useId } from 'react';
+import { graphql, ResultOf } from '~/client/graphql';
import { Link } from '~/components/link';
import { cn } from '~/lib/utils';
import { BcImage } from '../bc-image';
-import { Pricing } from '../pricing';
+import { Pricing, PricingFragment } from '../pricing';
import { Cart } from './cart';
+import { CartFragment } from './cart/fragment';
import { Compare } from './compare';
-export interface Product {
- entityId: number;
- name: string;
- defaultImage?: {
- altText?: string;
- url?: string;
- } | null;
- path: string;
- brand?: {
- name: string;
- path: string;
- } | null;
- prices?: {
- price?: {
- value?: number;
- currencyCode?: string;
- };
- basePrice?: {
- value?: number;
- currencyCode?: string;
- } | null;
- retailPrice?: {
- value?: number;
- currencyCode?: string;
- } | null;
- salePrice?: {
- value?: number;
- currencyCode?: string;
- } | null;
- priceRange?: {
- min?: {
- value?: number;
- currencyCode?: string;
- } | null;
- max?: {
- value?: number;
- currencyCode?: string;
- } | null;
- } | null;
- } | null;
- reviewSummary?: {
- numberOfReviews: number;
- averageRating: number;
- } | null;
- productOptions?: Array<{
- entityId: number;
- }>;
-}
+export const ProductCardFragment = graphql(
+ `
+ fragment ProductCardFragment on Product {
+ entityId
+ name
+ defaultImage {
+ altText
+ url: urlTemplate
+ }
+ path
+ brand {
+ name
+ path
+ }
+ reviewSummary {
+ numberOfReviews
+ averageRating
+ }
+ ...CartFragment
+ ...PricingFragment
+ }
+ `,
+ [PricingFragment, CartFragment],
+);
-interface ProductCardProps {
- product: Partial;
+interface Props {
+ product: ResultOf;
imageSize?: 'tall' | 'wide' | 'square';
imagePriority?: boolean;
showCart?: boolean;
@@ -84,7 +62,7 @@ export const ProductCard = ({
showCart = true,
showCompare = true,
showReviews = true,
-}: ProductCardProps) => {
+}: Props) => {
const summaryId = useId();
const t = useTranslations('Product.ProductSheet');
@@ -104,12 +82,12 @@ export const ProductCard = ({
>
{product.defaultImage ? (
) : (
@@ -131,7 +109,7 @@ export const ProductCard = ({
product.name
)}
- {product.reviewSummary && showReviews && (
+ {showReviews && (
-
+
+
{showCompare && (
)}
- {showCart && }
+ {showCart && }
);
};
diff --git a/apps/core/components/quick-search/index.tsx b/apps/core/components/quick-search/index.tsx
index e93a97143..aac5f7941 100644
--- a/apps/core/components/quick-search/index.tsx
+++ b/apps/core/components/quick-search/index.tsx
@@ -209,7 +209,7 @@ export const QuickSearch = ({ children, initialTerm = '' }: SearchProps) => {
{product.name}
-
+