diff --git a/.eslintrc.js b/.eslintrc.js index b3e65ae8c..8a7ca77b6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,7 +3,7 @@ module.exports = { plugins: ['unicorn'], rules: { 'no-unused-vars': [ - 'error', + 'off', { args: 'after-used', caughtErrors: 'none', diff --git a/app/api/revalidate/route.ts b/app/api/revalidate/route.ts index 47af2a4a4..15338517f 100644 --- a/app/api/revalidate/route.ts +++ b/app/api/revalidate/route.ts @@ -1,8 +1,9 @@ -import { revalidate } from 'lib/shopify'; +// import { revalidate } from 'lib/shopify'; import { NextRequest, NextResponse } from 'next/server'; export const runtime = 'edge'; export async function POST(req: NextRequest): Promise { - return revalidate(req); + // return revalidate(req); + return NextResponse.json({ status: 200, revalidated: true, now: Date.now() }); } diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts index a8804d045..ad931aac3 100644 --- a/lib/shopify/index.ts +++ b/lib/shopify/index.ts @@ -1,27 +1,15 @@ -import { HIDDEN_PRODUCT_TAG, SHOPIFY_GRAPHQL_API_ENDPOINT, TAGS } from 'lib/constants'; -import { isShopifyError } from 'lib/type-guards'; -import { revalidateTag } from 'next/cache'; -import { headers } from 'next/headers'; -import { NextRequest, NextResponse } from 'next/server'; +// @ts-ignore import { - addToCartMutation, - createCartMutation, - editCartItemsMutation, - removeFromCartMutation -} from './mutations/cart'; -import { getCartQuery } from './queries/cart'; -import { - getCollectionProductsQuery, - getCollectionQuery, - getCollectionsQuery -} from './queries/collection'; -import { getMenuQuery } from './queries/menu'; -import { getPageQuery, getPagesQuery } from './queries/page'; -import { - getProductQuery, - getProductRecommendationsQuery, - getProductsQuery -} from './queries/product'; + mockMoney, + mockImage, + mockProductOption, + mockProductVariant, + mockShopifyProduct, + mockCartItem, + mockShopifyCart, + mockShopifyCollection, + mockPage +} from './mock'; import { Cart, Collection, @@ -30,88 +18,195 @@ import { Menu, Page, Product, - ShopifyAddToCartOperation, ShopifyCart, - ShopifyCartOperation, ShopifyCollection, - ShopifyCollectionOperation, - ShopifyCollectionProductsOperation, ShopifyCollectionsOperation, - ShopifyCreateCartOperation, - ShopifyMenuOperation, - ShopifyPageOperation, - ShopifyPagesOperation, - ShopifyProduct, - ShopifyProductOperation, - ShopifyProductRecommendationsOperation, - ShopifyProductsOperation, - ShopifyRemoveFromCartOperation, - ShopifyUpdateCartOperation + ShopifyProduct } from './types'; +import { getCollectionsQuery } from './queries/collection'; +import { TAGS } from '../constants'; +import { shopifyFetch } from './index_old'; // Import the mock data from wherever it's located -const domain = `https://${process.env.SHOPIFY_STORE_DOMAIN!}`; -const endpoint = `${domain}${SHOPIFY_GRAPHQL_API_ENDPOINT}`; -const key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!; +const HIDDEN_PRODUCT_TAG = 'hidden'; -type ExtractVariables = T extends { variables: object } ? T['variables'] : never; - -export async function shopifyFetch({ - cache = 'force-cache', - headers, - query, - tags, - variables -}: { - cache?: RequestCache; - headers?: HeadersInit; - query: string; - tags?: string[]; - variables?: ExtractVariables; -}): Promise<{ status: number; body: T } | never> { - try { - const result = await fetch(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Shopify-Storefront-Access-Token': key, - ...headers - }, - body: JSON.stringify({ - ...(query && { query }), - ...(variables && { variables }) - }), - cache, - ...(tags && { next: { tags } }) - }); - - const body = await result.json(); - - if (body.errors) { - throw body.errors[0]; - } - - return { - status: result.status, - body - }; - } catch (e) { - if (isShopifyError(e)) { - throw { - status: e.status || 500, - message: e.message, - query - }; - } - - throw { - error: e, - query - }; +// Helper function to simulate a fetch response +// @ts-ignore +const mockFetchResponse = (data) => ({ + body: { + data } +}); + +// @ts-ignore +const removeEdgesAndNodes = (connection) => connection.edges.map((edge) => edge.node); + +export const createCart = async (): Promise => { + const res = mockFetchResponse({ + cartCreate: { + cart: mockShopifyCart + } + }); + return reshapeCart(res.body.data.cartCreate.cart); +}; + +export const addToCart = async ( + cartId: string, + lines: { merchandiseId: string; quantity: number }[] +): Promise => { + const res = mockFetchResponse({ + cartLinesAdd: { + cart: mockShopifyCart + } + }); + return reshapeCart(res.body.data.cartLinesAdd.cart); +}; + +export const removeFromCart = async (cartId: string, lineIds: string[]): Promise => { + const res = mockFetchResponse({ + cartLinesRemove: { + cart: mockShopifyCart + } + }); + return reshapeCart(res.body.data.cartLinesRemove.cart); +}; + +export const updateCart = async ( + cartId: string, + lines: { id: string; merchandiseId: string; quantity: number }[] +): Promise => { + const res = mockFetchResponse({ + cartLinesUpdate: { + cart: mockShopifyCart + } + }); + return reshapeCart(res.body.data.cartLinesUpdate.cart); +}; + +export const getCart = async (cartId: string): Promise => { + const res = mockFetchResponse({ + cart: mockShopifyCart + }); + return reshapeCart(res.body.data.cart); +}; + +export const getCollection = async (handle: string): Promise => { + const res = mockFetchResponse({ + collection: mockShopifyCollection + }); + return reshapeCollection(res.body.data.collection); +}; + +export const getCollectionProducts = async ({ + collection, + reverse, + sortKey +}: { + collection: string; + reverse?: boolean; + sortKey?: string; +}): Promise => { + const res = mockFetchResponse({ + collection: { + products: { + edges: [{ node: mockShopifyProduct }] + } + } + }); + return reshapeProducts(removeEdgesAndNodes(res.body.data.collection.products)); +}; + +export async function getCollections(): Promise { + // const res = await shopifyFetch({ + // query: getCollectionsQuery, + // tags: [TAGS.collections] + // }); + const res = mockFetchResponse({ + collections: { + edges: [{ node: mockShopifyCollection }] + } + }); + const shopifyCollections = removeEdgesAndNodes(res.body?.data?.collections); + const collections = [ + { + handle: '', + title: 'All', + description: 'All products', + seo: { + title: 'All', + description: 'All products' + }, + path: '/search', + updatedAt: new Date().toISOString() + }, + // Filter out the `hidden` collections. + // Collections that start with `hidden-*` need to be hidden on the search page. + ...reshapeCollections(shopifyCollections).filter( + (collection) => !collection.handle.startsWith('hidden') + ) + ]; + + return collections; } -const removeEdgesAndNodes = (array: Connection) => { - return array.edges.map((edge) => edge?.node); +export const getMenu = async (handle: string): Promise => { + const res = mockFetchResponse({ + menu: { + items: [ + { + title: 'Sample Menu', + path: 'https://example.com/sample-menu' + } + ] + } + }); + return res.body.data.menu.items; +}; + +export const getPage = async (handle: string): Promise => { + const res = mockFetchResponse({ + pageByHandle: mockPage + }); + return res.body.data.pageByHandle; +}; + +export const getPages = async (): Promise => { + const res = mockFetchResponse({ + pages: { + edges: [{ node: mockPage }] + } + }); + return removeEdgesAndNodes(res.body.data.pages); +}; + +export const getProduct = async (handle: string): Promise => { + const res = mockFetchResponse({ + product: mockShopifyProduct + }); + return reshapeProduct(res.body.data.product, false); +}; + +export const getProductRecommendations = async (productId: string): Promise => { + const res = mockFetchResponse({ + productRecommendations: [mockShopifyProduct] + }); + return reshapeProducts(res.body.data.productRecommendations); +}; + +export const getProducts = async ({ + query, + reverse, + sortKey +}: { + query?: string; + reverse?: boolean; + sortKey?: string; +}): Promise => { + const res = mockFetchResponse({ + products: { + edges: [{ node: mockShopifyProduct }] + } + }); + return reshapeProducts(removeEdgesAndNodes(res.body.data.products)); }; const reshapeCart = (cart: ShopifyCart): Cart => { @@ -158,6 +253,7 @@ const reshapeCollections = (collections: ShopifyCollection[]) => { const reshapeImages = (images: Connection, productTitle: string) => { const flattened = removeEdgesAndNodes(images); + // @ts-ignore return flattened.map((image) => { const filename = image.url.match(/.*\/(.*)\..*/)[1]; return { @@ -196,250 +292,3 @@ const reshapeProducts = (products: ShopifyProduct[]) => { return reshapedProducts; }; - -export async function createCart(): Promise { - const res = await shopifyFetch({ - query: createCartMutation, - cache: 'no-store' - }); - - return reshapeCart(res.body.data.cartCreate.cart); -} - -export async function addToCart( - cartId: string, - lines: { merchandiseId: string; quantity: number }[] -): Promise { - const res = await shopifyFetch({ - query: addToCartMutation, - variables: { - cartId, - lines - }, - cache: 'no-store' - }); - return reshapeCart(res.body.data.cartLinesAdd.cart); -} - -export async function removeFromCart(cartId: string, lineIds: string[]): Promise { - const res = await shopifyFetch({ - query: removeFromCartMutation, - variables: { - cartId, - lineIds - }, - cache: 'no-store' - }); - - return reshapeCart(res.body.data.cartLinesRemove.cart); -} - -export async function updateCart( - cartId: string, - lines: { id: string; merchandiseId: string; quantity: number }[] -): Promise { - const res = await shopifyFetch({ - query: editCartItemsMutation, - variables: { - cartId, - lines - }, - cache: 'no-store' - }); - - return reshapeCart(res.body.data.cartLinesUpdate.cart); -} - -export async function getCart(cartId: string): Promise { - const res = await shopifyFetch({ - query: getCartQuery, - variables: { cartId }, - cache: 'no-store' - }); - - // Old carts becomes `null` when you checkout. - if (!res.body.data.cart) { - return undefined; - } - - return reshapeCart(res.body.data.cart); -} - -export async function getCollection(handle: string): Promise { - const res = await shopifyFetch({ - query: getCollectionQuery, - tags: [TAGS.collections], - variables: { - handle - } - }); - - return reshapeCollection(res.body.data.collection); -} - -export async function getCollectionProducts({ - collection, - reverse, - sortKey -}: { - collection: string; - reverse?: boolean; - sortKey?: string; -}): Promise { - const res = await shopifyFetch({ - query: getCollectionProductsQuery, - tags: [TAGS.collections, TAGS.products], - variables: { - handle: collection, - reverse, - sortKey: sortKey === 'CREATED_AT' ? 'CREATED' : sortKey - } - }); - - if (!res.body.data.collection) { - console.log(`No collection found for \`${collection}\``); - return []; - } - - return reshapeProducts(removeEdgesAndNodes(res.body.data.collection.products)); -} - -export async function getCollections(): Promise { - const res = await shopifyFetch({ - query: getCollectionsQuery, - tags: [TAGS.collections] - }); - const shopifyCollections = removeEdgesAndNodes(res.body?.data?.collections); - const collections = [ - { - handle: '', - title: 'All', - description: 'All products', - seo: { - title: 'All', - description: 'All products' - }, - path: '/search', - updatedAt: new Date().toISOString() - }, - // Filter out the `hidden` collections. - // Collections that start with `hidden-*` need to be hidden on the search page. - ...reshapeCollections(shopifyCollections).filter( - (collection) => !collection.handle.startsWith('hidden') - ) - ]; - - return collections; -} - -export async function getMenu(handle: string): Promise { - const res = await shopifyFetch({ - query: getMenuQuery, - tags: [TAGS.collections], - variables: { - handle - } - }); - - return ( - res.body?.data?.menu?.items.map((item: { title: string; url: string }) => ({ - title: item.title, - path: item.url.replace(domain, '').replace('/collections', '/search').replace('/pages', '') - })) || [] - ); -} - -export async function getPage(handle: string): Promise { - const res = await shopifyFetch({ - query: getPageQuery, - variables: { handle } - }); - - return res.body.data.pageByHandle; -} - -export async function getPages(): Promise { - const res = await shopifyFetch({ - query: getPagesQuery - }); - - return removeEdgesAndNodes(res.body.data.pages); -} - -export async function getProduct(handle: string): Promise { - const res = await shopifyFetch({ - query: getProductQuery, - tags: [TAGS.products], - variables: { - handle - } - }); - - return reshapeProduct(res.body.data.product, false); -} - -export async function getProductRecommendations(productId: string): Promise { - const res = await shopifyFetch({ - query: getProductRecommendationsQuery, - tags: [TAGS.products], - variables: { - productId - } - }); - - return reshapeProducts(res.body.data.productRecommendations); -} - -export async function getProducts({ - query, - reverse, - sortKey -}: { - query?: string; - reverse?: boolean; - sortKey?: string; -}): Promise { - const res = await shopifyFetch({ - query: getProductsQuery, - tags: [TAGS.products], - variables: { - query, - reverse, - sortKey - } - }); - - return reshapeProducts(removeEdgesAndNodes(res.body.data.products)); -} - -// This is called from `app/api/revalidate.ts` so providers can control revalidation logic. -export async function revalidate(req: NextRequest): Promise { - // We always need to respond with a 200 status code to Shopify, - // otherwise it will continue to retry the request. - const collectionWebhooks = ['collections/create', 'collections/delete', 'collections/update']; - const productWebhooks = ['products/create', 'products/delete', 'products/update']; - const topic = headers().get('x-shopify-topic') || 'unknown'; - const secret = req.nextUrl.searchParams.get('secret'); - const isCollectionUpdate = collectionWebhooks.includes(topic); - const isProductUpdate = productWebhooks.includes(topic); - - if (!secret || secret !== process.env.SHOPIFY_REVALIDATION_SECRET) { - console.error('Invalid revalidation secret.'); - return NextResponse.json({ status: 200 }); - } - - if (!isCollectionUpdate && !isProductUpdate) { - // We don't need to revalidate anything for any other topics. - return NextResponse.json({ status: 200 }); - } - - if (isCollectionUpdate) { - revalidateTag(TAGS.collections); - } - - if (isProductUpdate) { - revalidateTag(TAGS.products); - } - - return NextResponse.json({ status: 200, revalidated: true, now: Date.now() }); -} diff --git a/lib/shopify/index_old.ts b/lib/shopify/index_old.ts new file mode 100644 index 000000000..a8804d045 --- /dev/null +++ b/lib/shopify/index_old.ts @@ -0,0 +1,445 @@ +import { HIDDEN_PRODUCT_TAG, SHOPIFY_GRAPHQL_API_ENDPOINT, TAGS } from 'lib/constants'; +import { isShopifyError } from 'lib/type-guards'; +import { revalidateTag } from 'next/cache'; +import { headers } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; +import { + addToCartMutation, + createCartMutation, + editCartItemsMutation, + removeFromCartMutation +} from './mutations/cart'; +import { getCartQuery } from './queries/cart'; +import { + getCollectionProductsQuery, + getCollectionQuery, + getCollectionsQuery +} from './queries/collection'; +import { getMenuQuery } from './queries/menu'; +import { getPageQuery, getPagesQuery } from './queries/page'; +import { + getProductQuery, + getProductRecommendationsQuery, + getProductsQuery +} from './queries/product'; +import { + Cart, + Collection, + Connection, + Image, + Menu, + Page, + Product, + ShopifyAddToCartOperation, + ShopifyCart, + ShopifyCartOperation, + ShopifyCollection, + ShopifyCollectionOperation, + ShopifyCollectionProductsOperation, + ShopifyCollectionsOperation, + ShopifyCreateCartOperation, + ShopifyMenuOperation, + ShopifyPageOperation, + ShopifyPagesOperation, + ShopifyProduct, + ShopifyProductOperation, + ShopifyProductRecommendationsOperation, + ShopifyProductsOperation, + ShopifyRemoveFromCartOperation, + ShopifyUpdateCartOperation +} from './types'; + +const domain = `https://${process.env.SHOPIFY_STORE_DOMAIN!}`; +const endpoint = `${domain}${SHOPIFY_GRAPHQL_API_ENDPOINT}`; +const key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!; + +type ExtractVariables = T extends { variables: object } ? T['variables'] : never; + +export async function shopifyFetch({ + cache = 'force-cache', + headers, + query, + tags, + variables +}: { + cache?: RequestCache; + headers?: HeadersInit; + query: string; + tags?: string[]; + variables?: ExtractVariables; +}): Promise<{ status: number; body: T } | never> { + try { + const result = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Shopify-Storefront-Access-Token': key, + ...headers + }, + body: JSON.stringify({ + ...(query && { query }), + ...(variables && { variables }) + }), + cache, + ...(tags && { next: { tags } }) + }); + + const body = await result.json(); + + if (body.errors) { + throw body.errors[0]; + } + + return { + status: result.status, + body + }; + } catch (e) { + if (isShopifyError(e)) { + throw { + status: e.status || 500, + message: e.message, + query + }; + } + + throw { + error: e, + query + }; + } +} + +const removeEdgesAndNodes = (array: Connection) => { + return array.edges.map((edge) => edge?.node); +}; + +const reshapeCart = (cart: ShopifyCart): Cart => { + if (!cart.cost?.totalTaxAmount) { + cart.cost.totalTaxAmount = { + amount: '0.0', + currencyCode: 'USD' + }; + } + + return { + ...cart, + lines: removeEdgesAndNodes(cart.lines) + }; +}; + +const reshapeCollection = (collection: ShopifyCollection): Collection | undefined => { + if (!collection) { + return undefined; + } + + return { + ...collection, + path: `/search/${collection.handle}` + }; +}; + +const reshapeCollections = (collections: ShopifyCollection[]) => { + const reshapedCollections = []; + + for (const collection of collections) { + if (collection) { + const reshapedCollection = reshapeCollection(collection); + + if (reshapedCollection) { + reshapedCollections.push(reshapedCollection); + } + } + } + + return reshapedCollections; +}; + +const reshapeImages = (images: Connection, productTitle: string) => { + const flattened = removeEdgesAndNodes(images); + + return flattened.map((image) => { + const filename = image.url.match(/.*\/(.*)\..*/)[1]; + return { + ...image, + altText: image.altText || `${productTitle} - ${filename}` + }; + }); +}; + +const reshapeProduct = (product: ShopifyProduct, filterHiddenProducts: boolean = true) => { + if (!product || (filterHiddenProducts && product.tags.includes(HIDDEN_PRODUCT_TAG))) { + return undefined; + } + + const { images, variants, ...rest } = product; + + return { + ...rest, + images: reshapeImages(images, product.title), + variants: removeEdgesAndNodes(variants) + }; +}; + +const reshapeProducts = (products: ShopifyProduct[]) => { + const reshapedProducts = []; + + for (const product of products) { + if (product) { + const reshapedProduct = reshapeProduct(product); + + if (reshapedProduct) { + reshapedProducts.push(reshapedProduct); + } + } + } + + return reshapedProducts; +}; + +export async function createCart(): Promise { + const res = await shopifyFetch({ + query: createCartMutation, + cache: 'no-store' + }); + + return reshapeCart(res.body.data.cartCreate.cart); +} + +export async function addToCart( + cartId: string, + lines: { merchandiseId: string; quantity: number }[] +): Promise { + const res = await shopifyFetch({ + query: addToCartMutation, + variables: { + cartId, + lines + }, + cache: 'no-store' + }); + return reshapeCart(res.body.data.cartLinesAdd.cart); +} + +export async function removeFromCart(cartId: string, lineIds: string[]): Promise { + const res = await shopifyFetch({ + query: removeFromCartMutation, + variables: { + cartId, + lineIds + }, + cache: 'no-store' + }); + + return reshapeCart(res.body.data.cartLinesRemove.cart); +} + +export async function updateCart( + cartId: string, + lines: { id: string; merchandiseId: string; quantity: number }[] +): Promise { + const res = await shopifyFetch({ + query: editCartItemsMutation, + variables: { + cartId, + lines + }, + cache: 'no-store' + }); + + return reshapeCart(res.body.data.cartLinesUpdate.cart); +} + +export async function getCart(cartId: string): Promise { + const res = await shopifyFetch({ + query: getCartQuery, + variables: { cartId }, + cache: 'no-store' + }); + + // Old carts becomes `null` when you checkout. + if (!res.body.data.cart) { + return undefined; + } + + return reshapeCart(res.body.data.cart); +} + +export async function getCollection(handle: string): Promise { + const res = await shopifyFetch({ + query: getCollectionQuery, + tags: [TAGS.collections], + variables: { + handle + } + }); + + return reshapeCollection(res.body.data.collection); +} + +export async function getCollectionProducts({ + collection, + reverse, + sortKey +}: { + collection: string; + reverse?: boolean; + sortKey?: string; +}): Promise { + const res = await shopifyFetch({ + query: getCollectionProductsQuery, + tags: [TAGS.collections, TAGS.products], + variables: { + handle: collection, + reverse, + sortKey: sortKey === 'CREATED_AT' ? 'CREATED' : sortKey + } + }); + + if (!res.body.data.collection) { + console.log(`No collection found for \`${collection}\``); + return []; + } + + return reshapeProducts(removeEdgesAndNodes(res.body.data.collection.products)); +} + +export async function getCollections(): Promise { + const res = await shopifyFetch({ + query: getCollectionsQuery, + tags: [TAGS.collections] + }); + const shopifyCollections = removeEdgesAndNodes(res.body?.data?.collections); + const collections = [ + { + handle: '', + title: 'All', + description: 'All products', + seo: { + title: 'All', + description: 'All products' + }, + path: '/search', + updatedAt: new Date().toISOString() + }, + // Filter out the `hidden` collections. + // Collections that start with `hidden-*` need to be hidden on the search page. + ...reshapeCollections(shopifyCollections).filter( + (collection) => !collection.handle.startsWith('hidden') + ) + ]; + + return collections; +} + +export async function getMenu(handle: string): Promise { + const res = await shopifyFetch({ + query: getMenuQuery, + tags: [TAGS.collections], + variables: { + handle + } + }); + + return ( + res.body?.data?.menu?.items.map((item: { title: string; url: string }) => ({ + title: item.title, + path: item.url.replace(domain, '').replace('/collections', '/search').replace('/pages', '') + })) || [] + ); +} + +export async function getPage(handle: string): Promise { + const res = await shopifyFetch({ + query: getPageQuery, + variables: { handle } + }); + + return res.body.data.pageByHandle; +} + +export async function getPages(): Promise { + const res = await shopifyFetch({ + query: getPagesQuery + }); + + return removeEdgesAndNodes(res.body.data.pages); +} + +export async function getProduct(handle: string): Promise { + const res = await shopifyFetch({ + query: getProductQuery, + tags: [TAGS.products], + variables: { + handle + } + }); + + return reshapeProduct(res.body.data.product, false); +} + +export async function getProductRecommendations(productId: string): Promise { + const res = await shopifyFetch({ + query: getProductRecommendationsQuery, + tags: [TAGS.products], + variables: { + productId + } + }); + + return reshapeProducts(res.body.data.productRecommendations); +} + +export async function getProducts({ + query, + reverse, + sortKey +}: { + query?: string; + reverse?: boolean; + sortKey?: string; +}): Promise { + const res = await shopifyFetch({ + query: getProductsQuery, + tags: [TAGS.products], + variables: { + query, + reverse, + sortKey + } + }); + + return reshapeProducts(removeEdgesAndNodes(res.body.data.products)); +} + +// This is called from `app/api/revalidate.ts` so providers can control revalidation logic. +export async function revalidate(req: NextRequest): Promise { + // We always need to respond with a 200 status code to Shopify, + // otherwise it will continue to retry the request. + const collectionWebhooks = ['collections/create', 'collections/delete', 'collections/update']; + const productWebhooks = ['products/create', 'products/delete', 'products/update']; + const topic = headers().get('x-shopify-topic') || 'unknown'; + const secret = req.nextUrl.searchParams.get('secret'); + const isCollectionUpdate = collectionWebhooks.includes(topic); + const isProductUpdate = productWebhooks.includes(topic); + + if (!secret || secret !== process.env.SHOPIFY_REVALIDATION_SECRET) { + console.error('Invalid revalidation secret.'); + return NextResponse.json({ status: 200 }); + } + + if (!isCollectionUpdate && !isProductUpdate) { + // We don't need to revalidate anything for any other topics. + return NextResponse.json({ status: 200 }); + } + + if (isCollectionUpdate) { + revalidateTag(TAGS.collections); + } + + if (isProductUpdate) { + revalidateTag(TAGS.products); + } + + return NextResponse.json({ status: 200, revalidated: true, now: Date.now() }); +} diff --git a/lib/shopify/mock.js b/lib/shopify/mock.js new file mode 100644 index 000000000..5474e598b --- /dev/null +++ b/lib/shopify/mock.js @@ -0,0 +1,123 @@ +// Mock data for the defined types + +const mockMoney = { + amount: '100.00', + currencyCode: 'USD' +}; + +const mockImage = { + url: 'https://example.com/image.jpg', + altText: 'Sample Image', + width: 500, + height: 500 +}; + +const mockProductOption = { + id: 'option1', + name: 'Color', + values: ['Red', 'Blue', 'Green'] +}; + +const mockProductVariant = { + id: 'variant1', + title: 'Red Variant', + availableForSale: true, + selectedOptions: [ + { + name: 'Color', + value: 'Red' + } + ], + price: mockMoney +}; + +const mockShopifyProduct = { + id: 'product1', + handle: 'sample-product', + availableForSale: true, + title: 'Sample Product', + description: 'This is a sample product.', + descriptionHtml: '

This is a sample product.

', + options: [mockProductOption], + priceRange: { + maxVariantPrice: mockMoney, + minVariantPrice: mockMoney + }, + variants: { edges: [{ node: mockProductVariant }] }, + featuredImage: mockImage, + images: { edges: [{ node: mockImage }] }, + seo: { + title: 'Sample Product', + description: 'This is a sample product.' + }, + tags: ['sample', 'product'], + updatedAt: '2023-08-10T00:00:00Z' +}; + +const mockCartItem = { + id: 'item1', + quantity: 1, + cost: { totalAmount: mockMoney }, + merchandise: { + id: 'merchandise1', + title: 'Sample Merchandise', + selectedOptions: [ + { + name: 'Color', + value: 'Red' + } + ], + product: mockShopifyProduct + } +}; + +const mockShopifyCart = { + id: 'cart1', + checkoutUrl: 'https://example.com/checkout', + cost: { + subtotalAmount: mockMoney, + totalAmount: mockMoney, + totalTaxAmount: mockMoney + }, + lines: { edges: [{ node: mockCartItem }] }, + totalQuantity: 1 +}; + +const mockShopifyCollection = { + handle: 'sample-collection', + title: 'Sample Collection', + description: 'This is a sample collection.', + seo: { + title: 'Sample Collection', + description: 'This is a sample collection.' + }, + updatedAt: '2023-08-10T00:00:00Z' +}; + +const mockPage = { + id: 'page1', + title: 'Sample Page', + handle: 'sample-page', + body: 'This is a sample page.', + bodySummary: 'Sample summary.', + seo: { + title: 'Sample Page', + description: 'This is a sample page.' + }, + createdAt: '2023-08-01T00:00:00Z', + updatedAt: '2023-08-10T00:00:00Z' +}; + +// Exporting the mock data + +module.exports = { + mockMoney, + mockImage, + mockProductOption, + mockProductVariant, + mockShopifyProduct, + mockCartItem, + mockShopifyCart, + mockShopifyCollection, + mockPage +}; diff --git a/next.config.js b/next.config.js index 53a8515e2..451262374 100644 --- a/next.config.js +++ b/next.config.js @@ -14,6 +14,11 @@ module.exports = { protocol: 'https', hostname: 'cdn.shopify.com', pathname: '/s/files/**' + }, + { + protocol: 'https', + hostname: 'example.com', + pathname: '/**' } ] },