-
- {collection.title}
-
-
- {collection.description && (
-
- {collection.description}
-
- )}
-
-
- EXPLORE COLLECTION
-
+ {/* Collection Image */}
+
+ {collection.image ? (
+

+ ) : (
+
+
-
+ )}
+
+
+ {/* Collection Info */}
+
+
+ {collection.title}
+
);
diff --git a/components/shopify/collection-detail.tsx b/components/shopify/collection-detail.tsx
index d35c733..254e942 100644
--- a/components/shopify/collection-detail.tsx
+++ b/components/shopify/collection-detail.tsx
@@ -4,11 +4,20 @@ import React from 'react';
import { useParams } from 'next/navigation';
import { useCollectionProducts } from '@/hooks/use-shopify-collections';
import ProductCard from './product-card';
+import { Button } from '@/components/ui/button';
+
+const GRID_CLASSES =
+ 'grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-x-8 gap-y-16';
+
+const CollectionTitle: React.FC<{ title: string }> = ({ title }) => (
+
+ {title}
+
+);
const CollectionDetail: React.FC = () => {
const params = useParams();
const handle = params?.handle as string;
- console.log('[CollectionDetail] params:', params, 'handle:', handle);
const { collection, loading, error, refetch } = useCollectionProducts(handle);
@@ -19,25 +28,18 @@ const CollectionDetail: React.FC = () => {
if (loading) {
return (
-
-
-
- {formattedTitle}
-
+
+
+
{/* Loading Skeleton */}
-
+
{Array.from({ length: 8 }).map((_, index) => (
-
-
-
-
-
-
-
+
))}
@@ -49,25 +51,13 @@ const CollectionDetail: React.FC = () => {
if (error) {
return (
-
-
-
- {formattedTitle}
-
-
-
-
-
- Failed to Load Collection
-
-
{error}
-
-
+
+
+
+
{error}
+
);
@@ -77,26 +67,16 @@ const CollectionDetail: React.FC = () => {
const title = collection?.title || formattedTitle;
return (
-
-
-
- {title}
-
+
+
+
{products.length === 0 ? (
-
-
-
-
- No Products in Collection
-
-
- This collection doesn't have any products yet.
-
-
-
+
+ This collection doesn't have any products yet.
+
) : (
-
+
{products.map((product) => (
))}
diff --git a/components/shopify/collections.tsx b/components/shopify/collections.tsx
index 38668ca..1b73d09 100644
--- a/components/shopify/collections.tsx
+++ b/components/shopify/collections.tsx
@@ -7,40 +7,45 @@ import { Button } from '@/components/ui/button';
interface CollectionsProps {
title?: string;
+ subtitle?: string;
}
+const SectionHeader: React.FC<{ title: string; subtitle?: string }> = ({
+ title,
+ subtitle,
+}) => (
+ <>
+
+ {title}
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+ >
+);
+
+const GRID_CLASSES = 'grid grid-cols-2 lg:grid-cols-4 gap-x-8 gap-y-12';
+
const Collections: React.FC
= ({
title = 'Our Collections',
+ subtitle = 'Discover our carefully crafted worlds',
}) => {
const { collections, loading, error, refetch } = useCollections(12);
if (loading) {
return (
-
+
-
-
- {title}
-
-
- Handpicked stories and themes
-
+
-
- {Array.from({ length: 6 }).map((_, index) => (
-
-
-
-
-
-
+
+ {Array.from({ length: 8 }).map((_, index) => (
+
))}
@@ -52,21 +57,13 @@ const Collections: React.FC
= ({
if (error) {
return (
-
+
-
- {title}
-
-
-
-
- Unable to load collections
-
-
{error}
-
-
+
+
{error}
+
);
@@ -74,41 +71,23 @@ const Collections: React.FC
= ({
if (collections.length === 0) {
return (
-
+
-
- {title}
-
-
-
-
- No collections found
-
-
- Collections will appear here once added to your Shopify store.
-
-
+
+
+ Collections will appear here once added to your Shopify store.
+
);
}
return (
-
+
-
-
- {title}
-
-
- Discover our carefully crafted worlds
-
+
-
+
{collections.map((collection) => (
))}
diff --git a/components/shopify/header.tsx b/components/shopify/header.tsx
index f5c0a2f..1bf10b9 100644
--- a/components/shopify/header.tsx
+++ b/components/shopify/header.tsx
@@ -5,6 +5,7 @@ import Link from 'next/link';
import { useCartStore } from '@/hooks/use-shopify-cart';
import CartDrawer from '@/components/shopify/cart-drawer';
import { RiShoppingBagLine, RiCloseLine, RiMenu3Line } from '@remixicon/react';
+import { Button } from '@/components/ui/button';
const CartIcon: React.FC = () => {
const toggleCart = useCartStore((s) => s.toggleCart);
@@ -13,10 +14,12 @@ const CartIcon: React.FC = () => {
cart?.lines?.edges?.reduce((sum, { node }) => sum + node.quantity, 0) ?? 0;
return (
-
+
);
};
@@ -40,7 +43,7 @@ interface HeaderProps {
}
const Header: React.FC = ({
- storeName = 'STRIDE',
+ storeName = 'Shop',
logoUrl,
links = [
{ label: 'Shop', url: '/' },
@@ -62,7 +65,7 @@ const Header: React.FC = ({
className="h-6 w-auto object-contain"
/>
) : (
-
+
{storeName}
)}
@@ -86,13 +89,15 @@ const Header: React.FC = ({
{/* Mobile hamburger */}
-
+
diff --git a/components/shopify/product-card.tsx b/components/shopify/product-card.tsx
index a52c62f..8e22315 100644
--- a/components/shopify/product-card.tsx
+++ b/components/shopify/product-card.tsx
@@ -80,13 +80,13 @@ const ProductCard: React.FC
= ({ product }) => {
)}
{hasDiscount && compareAtPrice && (
-
+
SALE
)}
{!isAvailable && (
-
+
SOLD OUT
)}
diff --git a/components/shopify/product-detail/product-detail-gallery.tsx b/components/shopify/product-detail/product-detail-gallery.tsx
index bfd6664..c64ff8d 100644
--- a/components/shopify/product-detail/product-detail-gallery.tsx
+++ b/components/shopify/product-detail/product-detail-gallery.tsx
@@ -2,6 +2,7 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { RiCloseLine } from '@remixicon/react';
+import { Button } from '@/components/ui/button';
interface ProductImage {
url: string;
@@ -95,7 +96,7 @@ const ProductDetailGallery: React.FC = ({
key={index}
onClick={() => setZoomedIndex(index)}
aria-label={`Zoom ${image.altText || 'product image'}`}
- className={`relative aspect-square w-full shrink-0 snap-center overflow-hidden bg-white cursor-zoom-in sm:shrink ${
+ className={`relative aspect-square w-full shrink-0 snap-center overflow-hidden bg-white pointer-events-none sm:pointer-events-auto sm:shrink sm:cursor-zoom-in ${
isSingle ? 'sm:col-span-2' : ''
}`}
>
@@ -142,13 +143,15 @@ const ProductDetailGallery: React.FC = ({
className="max-h-full max-w-full object-contain bg-white"
/>
-
+
)}
>
diff --git a/components/shopify/product-detail/product-detail-info.tsx b/components/shopify/product-detail/product-detail-info.tsx
index 5407adf..7b8a98b 100644
--- a/components/shopify/product-detail/product-detail-info.tsx
+++ b/components/shopify/product-detail/product-detail-info.tsx
@@ -1,7 +1,8 @@
import React from 'react';
+import { Button } from '@/components/ui/button';
import { Loader } from '@/app/components/ui/loader';
import { RiSubtractLine, RiAddLine } from '@remixicon/react';
-import ShopPayLogo from '@/components/shopify/shop-pay-logo';
+import ShopPayButton from '@/components/shopify/shop-pay-button';
import type { ProductOption, ProductOptionValue } from '@/hooks/use-shopify-products';
import { isSwatchOptionName, swatchColorForName } from '@/config/swatches';
@@ -147,13 +148,15 @@ const ProductDetailInfo: React.FC
= ({
const { background, image } = swatchStyle(value);
return (
-
+
);
}
return (
-
+
);
})}
@@ -193,48 +197,50 @@ const ProductDetailInfo: React.FC
= ({
{/* Quantity + Add to Cart */}
-
+
{quantity}
-
+
-
+
- {/* Sends the shopper to the Shopify checkout, where Shop Pay is offered. */}
- {handleBuyNow && (
-
- )}
+ {/* Cart permalink into Shop Pay; falls back to the cart checkout URL. */}
+
{/* Description */}
{(product.descriptionHtml || product.description) && (
diff --git a/components/shopify/products.tsx b/components/shopify/products.tsx
index 5f4d285..82c19f9 100644
--- a/components/shopify/products.tsx
+++ b/components/shopify/products.tsx
@@ -2,7 +2,7 @@
import React, { useState, useEffect } from 'react';
import ProductCard from './product-card';
-import { getProducts } from '@/hooks/use-shopify-products';
+import { getProductsPage } from '@/hooks/use-shopify-products';
import { Button } from '@/components/ui/button';
import { Loader } from '@/app/components/ui/loader';
@@ -64,11 +64,11 @@ const Products: React.FC = ({
const [error, setError] = useState(null);
const [loadingMore, setLoadingMore] = useState(false);
const [hasMoreProducts, setHasMoreProducts] = useState(true);
+ const [cursor, setCursor] = useState(null);
- const fetchProducts = async (
- currentProducts: Product[] = [],
- loadMore = false
- ) => {
+ // Paging is cursor-based: without `after`, Shopify returns the same first
+ // page every time and "load more" appends nothing.
+ const fetchProducts = async (loadMore = false) => {
try {
if (loadMore) {
setLoadingMore(true);
@@ -77,27 +77,25 @@ const Products: React.FC = ({
setError(null);
}
- const newProducts = await getProducts({
+ const page = await getProductsPage({
first: limit,
+ after: loadMore ? cursor : null,
sortKey: 'CREATED_AT',
reverse: true,
});
- if (loadMore) {
- const existingIds = new Set(currentProducts.map((p) => p.id));
- const uniqueNewProducts = newProducts.filter(
- (p) => !existingIds.has(p.id)
- );
+ setProducts((prev) => {
+ if (!loadMore) return page.products;
- if (uniqueNewProducts.length === 0) {
- setHasMoreProducts(false);
- } else {
- setProducts((prev) => [...prev, ...uniqueNewProducts]);
- }
- } else {
- setProducts(newProducts);
- setHasMoreProducts(newProducts.length === limit);
- }
+ const existingIds = new Set(prev.map((p) => p.id));
+ return [
+ ...prev,
+ ...page.products.filter((p) => !existingIds.has(p.id)),
+ ];
+ });
+
+ setCursor(page.endCursor);
+ setHasMoreProducts(page.hasNextPage);
} catch (err) {
console.error('Error fetching products:', err);
setError(err instanceof Error ? err.message : 'Failed to load products');
@@ -113,7 +111,7 @@ const Products: React.FC = ({
const handleLoadMore = () => {
if (!loadingMore && hasMoreProducts) {
- fetchProducts(products, true);
+ fetchProducts(true);
}
};
@@ -146,7 +144,7 @@ const Products: React.FC = ({
if (error || products.length === 0) {
return (
-
+
{title}
@@ -154,21 +152,20 @@ const Products: React.FC = ({
{subtitle}
-
-
-
- {error ? 'Connection Error' : 'Coming Soon'}
-
-
- {error ||
- 'Our curated collection is being prepared. Please check back shortly.'}
-
- {error && (
-
- )}
-
+
+ {error ||
+ 'Our curated collection is being prepared. Please check back shortly.'}
+
+ {error && (
+
+ )}
);
@@ -195,17 +192,12 @@ const Products: React.FC
= ({
)}
diff --git a/components/shopify/shop-pay-button.tsx b/components/shopify/shop-pay-button.tsx
index 6c1343d..4ba680f 100644
--- a/components/shopify/shop-pay-button.tsx
+++ b/components/shopify/shop-pay-button.tsx
@@ -1,14 +1,10 @@
'use client';
-import React, { useEffect, useRef, useState } from 'react';
+import React from 'react';
import { SHOPIFY_STORE_DOMAIN } from '@/services/shopify/client';
-
-const SHOP_JS_URL =
- 'https://cdn.shopify.com/shopifycloud/shop-js/modules/v2/loader.pay-button.esm.js';
-const TAG_NAME = 'shop-pay-button';
-// The hosted element renders at ~43px; reserve it so nothing shifts while shop-js loads.
-const MIN_HEIGHT = '43px';
-const LOAD_TIMEOUT_MS = 10000;
+import ShopPayLogo from '@/components/shopify/shop-pay-logo';
+import { Loader } from '@/app/components/ui/loader';
+import { Button } from '@/components/ui/button';
export interface ShopPayVariant {
id: string;
@@ -18,43 +14,13 @@ export interface ShopPayVariant {
interface ShopPayButtonProps {
variants: ShopPayVariant[];
disabled?: boolean;
+ loading?: boolean;
className?: string;
- width?: string;
- borderRadius?: string;
- fallback?: React.ReactNode;
+ /** Used when no permalink can be built (missing domain or unusable IDs). */
+ onFallbackClick?: () => void;
}
-let shopJsPromise: Promise | null = null;
-
-function loadShopJs(): Promise {
- if (shopJsPromise) return shopJsPromise;
-
- shopJsPromise = new Promise((resolve, reject) => {
- if (!document.querySelector(`script[src="${SHOP_JS_URL}"]`)) {
- const script = document.createElement('script');
- script.src = SHOP_JS_URL;
- script.type = 'module';
- script.addEventListener('error', () =>
- reject(new Error('Failed to load the Shop Pay script.'))
- );
- document.head.appendChild(script);
- }
-
- // Element registration, not script load, is the real readiness signal.
- const timeout = setTimeout(
- () => reject(new Error('Timed out waiting for the Shop Pay button.')),
- LOAD_TIMEOUT_MS
- );
- customElements.whenDefined(TAG_NAME).then(() => {
- clearTimeout(timeout);
- resolve();
- }, reject);
- });
-
- return shopJsPromise;
-}
-
-// Storefront API IDs arrive as GIDs; the web component wants the bare numeric ID.
+// Cart permalinks need the bare numeric ID; the Storefront API returns GIDs.
function toNumericVariantId(id: string): string | null {
const trimmed = id.trim();
const gid = trimmed.match(/^gid:\/\/shopify\/ProductVariant\/(\d+)/);
@@ -62,18 +28,6 @@ function toNumericVariantId(id: string): string | null {
return /^\d+$/.test(trimmed) ? trimmed : null;
}
-function toVariantsAttribute(variants: ShopPayVariant[]): string | null {
- if (variants.length === 0) return null;
-
- const parts: string[] = [];
- for (const { id, quantity = 1 } of variants) {
- const numericId = toNumericVariantId(id);
- if (!numericId || !Number.isInteger(quantity) || quantity < 1) return null;
- parts.push(`${numericId}:${quantity}`);
- }
- return parts.join(',');
-}
-
function toStoreUrl(domain?: string): string | null {
if (!domain) return null;
try {
@@ -84,66 +38,62 @@ function toStoreUrl(domain?: string): string | null {
}
}
+// https://{shop}/cart/{variantId}:{qty},{variantId}:{qty}?payment=shop_pay
+// Loads the cart and drops the buyer straight into the Shop Pay checkout.
+export function buildShopPayUrl(variants: ShopPayVariant[]): string | null {
+ const storeUrl = toStoreUrl(SHOPIFY_STORE_DOMAIN);
+ if (!storeUrl || variants.length === 0) return null;
+
+ const lines: string[] = [];
+ for (const { id, quantity = 1 } of variants) {
+ const numericId = toNumericVariantId(id);
+ if (!numericId || !Number.isInteger(quantity) || quantity < 1) return null;
+ lines.push(`${numericId}:${quantity}`);
+ }
+
+ return `${storeUrl}/cart/${lines.join(',')}?payment=shop_pay`;
+}
+
+const BUTTON_CLASSES =
+ 'flex h-11 w-full cursor-pointer items-center justify-center rounded-md bg-shop px-4 text-white transition-colors hover:bg-shop/85 disabled:cursor-not-allowed disabled:opacity-50';
+
const ShopPayButton: React.FC = ({
variants,
disabled = false,
- className,
- width = '100%',
- borderRadius = '8px',
- fallback = null,
+ loading = false,
+ className = '',
+ onFallbackClick,
}) => {
- const containerRef = useRef(null);
- const [status, setStatus] = useState<'loading' | 'ready' | 'error'>(
- 'loading'
+ const shopPayUrl = buildShopPayUrl(variants);
+ const contents = (
+ <>
+ Buy with
+ {loading ? : }
+ >
);
- const storeUrl = toStoreUrl(SHOPIFY_STORE_DOMAIN);
- const variantsAttribute = toVariantsAttribute(variants);
-
- useEffect(() => {
- if (!storeUrl || !variantsAttribute) return;
-
- let cancelled = false;
- loadShopJs().then(
- () => !cancelled && setStatus('ready'),
- () => !cancelled && setStatus('error')
+ // An anchor keeps the checkout URL visible, openable in a new tab, and
+ // navigable without JS; the button only stands in when there's no URL.
+ if (shopPayUrl && !disabled) {
+ return (
+
+ {contents}
+
);
-
- return () => {
- cancelled = true;
- };
- }, [storeUrl, variantsAttribute]);
-
- useEffect(() => {
- const container = containerRef.current;
- if (status !== 'ready' || !container || !storeUrl || !variantsAttribute) {
- return;
- }
-
- const button = document.createElement(TAG_NAME);
- button.setAttribute('store-url', storeUrl);
- button.setAttribute('variants', variantsAttribute);
- button.setAttribute('channel', 'headless');
- if (disabled) button.setAttribute('disabled', '');
- button.style.setProperty('--shop-pay-button-width', width);
- button.style.setProperty('--shop-pay-button-border-radius', borderRadius);
- container.appendChild(button);
-
- return () => {
- button.remove();
- };
- }, [status, storeUrl, variantsAttribute, disabled, width, borderRadius]);
-
- if (!storeUrl || !variantsAttribute || status === 'error') {
- return <>{fallback}>;
}
return (
-
+
);
};
diff --git a/graphql/cart.js b/graphql/cart.js
index 1615db1..c70b616 100644
--- a/graphql/cart.js
+++ b/graphql/cart.js
@@ -4,6 +4,10 @@ const CartFragment = `
id
checkoutUrl
totalQuantity
+ discountCodes {
+ code
+ applicable
+ }
cost {
subtotalAmount {
amount
@@ -126,6 +130,22 @@ export const REMOVE_CART_LINES_MUTATION = `
}
`;
+// Apply (or clear) discount codes on the cart
+export const UPDATE_CART_DISCOUNT_CODES_MUTATION = `
+ ${CartFragment}
+ mutation UpdateCartDiscountCodes($cartId: ID!, $discountCodes: [String!]) {
+ cartDiscountCodesUpdate(cartId: $cartId, discountCodes: $discountCodes) {
+ cart {
+ ...CartFragment
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+ }
+`;
+
// Get cart by ID
export const GET_CART_QUERY = `
${CartFragment}
diff --git a/graphql/products.js b/graphql/products.js
index eee6b2a..725b2fd 100644
--- a/graphql/products.js
+++ b/graphql/products.js
@@ -103,8 +103,8 @@ export const ProductFragment = `
// Get multiple products
export const GET_PRODUCTS_QUERY = `
${ProductFragment}
- query GetProducts($first: Int!, $query: String, $sortKey: ProductSortKeys, $reverse: Boolean) {
- products(first: $first, query: $query, sortKey: $sortKey, reverse: $reverse) {
+ query GetProducts($first: Int!, $after: String, $query: String, $sortKey: ProductSortKeys, $reverse: Boolean) {
+ products(first: $first, after: $after, query: $query, sortKey: $sortKey, reverse: $reverse) {
edges {
node {
...ProductFragment
diff --git a/hooks/use-shopify-cart.ts b/hooks/use-shopify-cart.ts
index 3507b9a..e41c250 100644
--- a/hooks/use-shopify-cart.ts
+++ b/hooks/use-shopify-cart.ts
@@ -7,6 +7,7 @@ import {
ADD_CART_LINES_MUTATION,
UPDATE_CART_LINES_MUTATION,
REMOVE_CART_LINES_MUTATION,
+ UPDATE_CART_DISCOUNT_CODES_MUTATION,
GET_CART_QUERY,
} from '@/graphql/cart';
import { useEffect } from 'react';
@@ -59,10 +60,16 @@ interface CartLine {
};
}
+export interface CartDiscountCode {
+ code: string;
+ applicable: boolean;
+}
+
export interface Cart {
id: string;
checkoutUrl: string;
totalQuantity: number;
+ discountCodes?: CartDiscountCode[];
cost: {
subtotalAmount: {
amount: string;
@@ -147,6 +154,24 @@ async function removeCartLinesApi(
return response.data.cartLinesRemove.cart;
}
+async function updateCartDiscountCodesApi(
+ cartId: string,
+ discountCodes: string[]
+): Promise {
+ const response = await shopifyFetch({
+ query: UPDATE_CART_DISCOUNT_CODES_MUTATION,
+ variables: { cartId, discountCodes },
+ });
+
+ if (response.data.cartDiscountCodesUpdate.userErrors.length > 0) {
+ throw new Error(
+ response.data.cartDiscountCodesUpdate.userErrors[0].message
+ );
+ }
+
+ return response.data.cartDiscountCodesUpdate.cart;
+}
+
async function getCartApi(cartId: string): Promise {
const response = await shopifyFetch({
query: GET_CART_QUERY,
@@ -186,6 +211,7 @@ interface CartState {
addItem: (variantId: string, quantity?: number) => Promise;
removeItem: (lineId: string) => Promise;
updateItemQuantity: (lineId: string, quantity: number) => Promise;
+ applyDiscountCode: (code: string) => Promise;
refreshCart: () => Promise;
}
@@ -312,6 +338,29 @@ export const useCartStore = create((set, get) => ({
}
},
+ // Apply a discount code. Shopify accepts unknown codes and reports them back
+ // as `applicable: false`, so callers should check the returned cart.
+ applyDiscountCode: async (code: string) => {
+ const { cartId } = get();
+ if (!cartId) throw new Error('No cart exists');
+
+ try {
+ set({ loading: true, error: null });
+ const trimmed = code.trim();
+ const updatedCart = await updateCartDiscountCodesApi(
+ cartId,
+ trimmed ? [trimmed] : []
+ );
+ set({ cart: updatedCart, loading: false });
+ return updatedCart;
+ } catch (err) {
+ const errorMessage =
+ err instanceof Error ? err.message : 'Failed to apply discount code';
+ set({ error: errorMessage, loading: false });
+ throw err;
+ }
+ },
+
// Refresh cart from Shopify
refreshCart: async () => {
const storedCartId = localStorage.getItem(CART_ID_KEY);
@@ -374,6 +423,7 @@ export function useShopifyCart() {
addItem: store.addItem,
removeItem: store.removeItem,
updateItemQuantity: store.updateItemQuantity,
+ applyDiscountCode: store.applyDiscountCode,
refreshCart: store.refreshCart,
};
}
diff --git a/hooks/use-shopify-products.ts b/hooks/use-shopify-products.ts
index dc8caae..97c9118 100644
--- a/hooks/use-shopify-products.ts
+++ b/hooks/use-shopify-products.ts
@@ -81,11 +81,19 @@ export interface Product {
interface UseProductsOptions {
first?: number;
+ /** Cursor from a previous page's `endCursor`; omit for the first page. */
+ after?: string | null;
query?: string;
sortKey?: 'BEST_SELLING' | 'CREATED_AT' | 'PRICE' | 'TITLE';
reverse?: boolean;
}
+export interface ProductsPage {
+ products: Product[];
+ hasNextPage: boolean;
+ endCursor: string | null;
+}
+
interface UseProductsReturn {
products: Product[];
loading: boolean;
@@ -94,18 +102,33 @@ interface UseProductsReturn {
}
// Fetch multiple products
-export async function getProducts({
+export async function getProducts(
+ options: UseProductsOptions = {}
+): Promise {
+ const { products } = await getProductsPage(options);
+ return products;
+}
+
+// Same fetch, but keeps the cursor so callers can page through the catalogue.
+export async function getProductsPage({
first = 20,
+ after = null,
query = '',
sortKey = 'BEST_SELLING',
reverse = false,
-}: UseProductsOptions = {}): Promise {
+}: UseProductsOptions = {}): Promise {
const response = await shopifyFetch({
query: GET_PRODUCTS_QUERY,
- variables: { first, query, sortKey, reverse },
+ variables: { first, after, query, sortKey, reverse },
});
- return response.data.products.edges.map((edge: { node: Product }) => edge.node);
+ const { edges, pageInfo } = response.data.products;
+
+ return {
+ products: edges.map((edge: { node: Product }) => edge.node),
+ hasNextPage: Boolean(pageInfo?.hasNextPage),
+ endCursor: pageInfo?.endCursor ?? null,
+ };
}
// Fetch a single product by handle