diff --git a/app/search/[collection]/page.tsx b/app/search/[collection]/page.tsx index e25542bfc..c0fc6816f 100644 --- a/app/search/[collection]/page.tsx +++ b/app/search/[collection]/page.tsx @@ -1,10 +1,11 @@ -import { getCollection, getCollectionProducts } from 'lib/shopify'; +import { getCollection } from 'lib/shopify'; import { Metadata } from 'next'; import { notFound } from 'next/navigation'; import Grid from 'components/grid'; import ProductGridItems from 'components/layout/product-grid-items'; import { defaultSort, sorting } from 'lib/constants'; +import { getCollectionProducts } from 'lib/fourthwall'; export async function generateMetadata({ params diff --git a/components/carousel.tsx b/components/carousel.tsx index 751cf4b48..a137bbb4f 100644 --- a/components/carousel.tsx +++ b/components/carousel.tsx @@ -1,4 +1,4 @@ -import { getCollectionProducts } from 'lib/shopify'; +import { getCollectionProducts } from 'lib/fourthwall'; import Link from 'next/link'; import { GridTileImage } from './grid/tile'; diff --git a/components/grid/three-items.tsx b/components/grid/three-items.tsx index 76df2ae4f..d37cdb9de 100644 --- a/components/grid/three-items.tsx +++ b/components/grid/three-items.tsx @@ -1,5 +1,5 @@ import { GridTileImage } from 'components/grid/tile'; -import { getCollectionProducts } from 'lib/shopify'; +import { getCollectionProducts } from 'lib/fourthwall'; import type { Product } from 'lib/shopify/types'; import Link from 'next/link'; diff --git a/components/layout/footer.tsx b/components/layout/footer.tsx index fc9336ea4..12fa8d67f 100644 --- a/components/layout/footer.tsx +++ b/components/layout/footer.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; import FooterMenu from 'components/layout/footer-menu'; import LogoSquare from 'components/logo-square'; -import { getMenu } from 'lib/shopify'; +import { getMenu } from 'lib/fourthwall'; import { Suspense } from 'react'; const { COMPANY_NAME, SITE_NAME } = process.env; diff --git a/components/layout/navbar/index.tsx b/components/layout/navbar/index.tsx index 6c7f3dead..f48584046 100644 --- a/components/layout/navbar/index.tsx +++ b/components/layout/navbar/index.tsx @@ -1,6 +1,6 @@ import CartModal from 'components/cart/modal'; import LogoSquare from 'components/logo-square'; -import { getMenu } from 'lib/shopify'; +import { getMenu } from 'lib/fourthwall'; import { Menu } from 'lib/shopify/types'; import Link from 'next/link'; import { Suspense } from 'react'; diff --git a/lib/fourthwall/index.ts b/lib/fourthwall/index.ts new file mode 100644 index 000000000..0b48af9a6 --- /dev/null +++ b/lib/fourthwall/index.ts @@ -0,0 +1,96 @@ +import { Menu, Product } from "lib/shopify/types"; +import { reshapeProducts } from "./reshape"; + +/** + * Helpers + */ +async function fourthwallGet(url: string, options: RequestInit = {}): Promise<{ status: number; body: T }> { + try { + const result = await fetch( + url, + { + method: 'GET', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + } + ); + + const body = await result.json(); + + return { + status: result.status, + body + }; + } catch (e) { + throw { + error: e, + url + }; + } +} + +async function fourthwallPost(url: string, data: any, options: RequestInit = {}): Promise<{ status: number; body: T }> { + try { + const result = await fetch(url, { + method: 'POST', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + }, + body: JSON.stringify(data) + }); + + const body = await result.json(); + + return { + status: result.status, + body + }; + } catch (e) { + throw { + error: e, + url, + data + }; + } +} + +/** + * Calls + */ + +export async function getCollectionProducts({ + collection, + reverse, + sortKey +}: { + collection: string; + reverse?: boolean; + sortKey?: string; +}): Promise { + const res = await fourthwallGet<{results: any[]}>(`${process.env.FW_URL}/api/public/v1.0/collections/${collection}/products?secret=${process.env.FW_SECRET}`, { + headers: { + 'X-ShopId': process.env.FW_SHOPID || '' + } + }); + + if (!res.body.results) { + console.log(`No collection found for \`${collection}\``); + return []; + } + + + return reshapeProducts(res.body.results); +} + + +/** + * Stubbed out + */ +export async function getMenu(handle: string): Promise { + return []; +} diff --git a/lib/fourthwall/reshape.ts b/lib/fourthwall/reshape.ts new file mode 100644 index 000000000..283bcd005 --- /dev/null +++ b/lib/fourthwall/reshape.ts @@ -0,0 +1,95 @@ +import { Image, Money, Product, ProductVariant } from "lib/shopify/types"; +import { FourthwallMoney, FourthwallProduct, FourthwallProductImage, FourthwallProductVariant } from "./types"; + +const DEFAULT_IMAGE: Image = { + url: '', + altText: '', + width: 0, + height: 0 +} + +export const reshapeProducts = (products: FourthwallProduct[]) => { + const reshapedProducts = []; + + for (const product of products) { + if (product) { + const reshapedProduct = reshapeProduct(product); + + if (reshapedProduct) { + reshapedProducts.push(reshapedProduct); + } + } + } + + return reshapedProducts; +}; + +const reshapeProduct = (product: FourthwallProduct): Product | undefined => { + if (!product) { + return undefined; + } + + const { images, variants, ...rest } = product; + + const minPrice = Math.min(...variants.map((v) => v.unitPrice.value)); + const maxPrice = Math.max(...variants.map((v) => v.unitPrice.value)); + + const currencyCode = variants[0]?.unitPrice.currencyCode || 'USD'; + + return { + ...rest, + handle: product.slug, + title: product.name, + descriptionHtml: product.description, + description: product.description, + images: reshapeImages(images, product.name), + variants: reshapeVariants(variants), + // stubbed out + availableForSale: true, + priceRange: { + minVariantPrice: { + amount: minPrice.toString(), + currencyCode, + }, + maxVariantPrice: { + amount: maxPrice.toString(), + currencyCode, + } + }, + options: [], + featuredImage: reshapeImages(images, product.name)[0] || DEFAULT_IMAGE, + seo: { + title: product.name, + description: product.description, + }, + tags: [], + updatedAt: new Date().toISOString(), + }; +}; + +const reshapeImages = (images: FourthwallProductImage[], productTitle: string): Image[] => { + return images.map((image) => { + const filename = image.url.match(/.*\/(.*)\..*/)?.[1]; + return { + ...image, + altText: `${productTitle} - ${filename}` + }; + }); +}; + +const reshapeVariants = (variants: FourthwallProductVariant[]): ProductVariant[] => { + return variants.map((v) => ({ + id: v.id, + title: v.name, + availableForSale: true, + selectedOptions: [], + price: reshapeMoney(v.unitPrice), + })) +} + +const reshapeMoney = (money: FourthwallMoney): Money => { + return { + amount: money.value.toString(), + currencyCode: money.currencyCode + }; +} diff --git a/lib/fourthwall/types.ts b/lib/fourthwall/types.ts new file mode 100644 index 000000000..8c8bfbd35 --- /dev/null +++ b/lib/fourthwall/types.ts @@ -0,0 +1,32 @@ +export type FourthwallMoney = { + value: number; + currencyCode: string; +} + +export type FourthwallProduct = { + id: string; + name: string; + slug: string; + description: string; + + images: FourthwallProductImage[]; + variants: FourthwallProductVariant[]; +}; + +export type FourthwallProductImage = { + id: string; + url: string; + width: number; + height: number; +}; + +export type FourthwallProductVariant = { + id: string; + name: string; + sku: string; + unitPrice: FourthwallMoney; + + images: FourthwallProductImage[]; + + // other attr +}; diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts index 9eca48bc0..38f80514c 100644 --- a/lib/shopify/index.ts +++ b/lib/shopify/index.ts @@ -12,11 +12,9 @@ import { } 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, @@ -28,7 +26,6 @@ import { Collection, Connection, Image, - Menu, Page, Product, ShopifyAddToCartOperation, @@ -36,10 +33,8 @@ import { ShopifyCartOperation, ShopifyCollection, ShopifyCollectionOperation, - ShopifyCollectionProductsOperation, ShopifyCollectionsOperation, ShopifyCreateCartOperation, - ShopifyMenuOperation, ShopifyPageOperation, ShopifyPagesOperation, ShopifyProduct, @@ -285,33 +280,6 @@ export async function getCollection(handle: 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, @@ -340,23 +308,6 @@ export async function getCollections(): Promise { 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,