From 1425889d24b653a75d0bacfc91dc5cb887fb6537 Mon Sep 17 00:00:00 2001 From: jpvalery Date: Thu, 2 Nov 2023 22:27:54 -0400 Subject: [PATCH] feat: graphql refactor (fixes #125) --- components/cart-item.js | 59 +++-- components/cart.js | 32 ++- components/header.js | 22 +- lib/context.js | 530 ++++++++++++++++++++++++---------------- 4 files changed, 389 insertions(+), 254 deletions(-) diff --git a/components/cart-item.js b/components/cart-item.js index 140792a..f2a3545 100644 --- a/components/cart-item.js +++ b/components/cart-item.js @@ -1,11 +1,8 @@ -import React from 'react' import Link from 'next/link' -import { hasObject } from '@lib/helpers' -import { useUpdateItem, useRemoveItem, useToggleCart } from '@lib/context' +import { useRemoveItem, useToggleCart, useUpdateItem } from '@lib/context' -import Photo from '@components/photo' import { ProductCounter, ProductPrice } from '@components/product' function CartItem({ item }) { @@ -14,24 +11,28 @@ function CartItem({ item }) { const toggleCart = useToggleCart() const changeQuantity = (quantity) => { - updateItem(item.lineID, quantity) + updateItem(item.id, quantity) } - const defaultPhoto = item.photos.cart?.find((set) => !set.forOption) - const variantPhoto = item.photos.cart?.find((set) => { - const option = set.forOption - ? { - name: set.forOption.split(':')[0], - value: set.forOption.split(':')[1], - } - : {} - return option.value && hasObject(item.options, option) - }) + /* + const defaultPhoto = item.photos.cart?.find((set) => !set.forOption); + const variantPhoto = item.photos.cart?.find((set) => { + const option = set.forOption + ? { + name: set.forOption.split(':')[0], + value: set.forOption.split(':')[1], + } + : {}; + return option.value && hasObject(item.options, option); + }); - const photos = variantPhoto ? variantPhoto : defaultPhoto + const photos = variantPhoto ? variantPhoto : defaultPhoto; + */ + const photos = item.merchandise.product.images.edges[0].node.originalSrc return (
+ {/* {photos && ( - )} + )} + */} + {photos && }
- +
@@ -70,11 +80,8 @@ function CartItem({ item }) { className="is-small is-inverted" />
-
diff --git a/components/cart.js b/components/cart.js index f623b79..93e400a 100644 --- a/components/cart.js +++ b/components/cart.js @@ -1,16 +1,19 @@ -import React, { useState, useEffect } from 'react' +import cx from 'classnames' import FocusTrap from 'focus-trap-react' import { m } from 'framer-motion' -import cx from 'classnames' +import { useEffect, useState } from 'react' import { centsToPrice } from '@lib/helpers' import { - useSiteContext, - useCartTotals, useCartCount, useCartItems, + useCartTotals, useCheckout, + useCheckoutCount, + useCheckoutItems, + useCheckoutTotals, + useSiteContext, useToggleCart, } from '@lib/context' @@ -23,8 +26,11 @@ const Cart = ({ data }) => { const { isCartOpen, isUpdating } = useSiteContext() const { subTotal } = useCartTotals() + const { subTotalCheckout } = useCheckoutTotals() + const checkouttCount = useCheckoutCount() const cartCount = useCartCount() - const lineItems = useCartItems() + const lineItems = useCheckoutItems() + const cartLineItems = useCartItems() const checkoutURL = useCheckout() const toggleCart = useToggleCart() @@ -52,7 +58,7 @@ const Cart = ({ data }) => { const buildCheckoutLink = shop.storeURL ? checkoutURL.replace( /^(?:https?:\/\/)?(?:[^@\/\n]+@)?(?:www\.)?([^:\/?\n]+)/g, - shop.storeURL + shop.storeURL, ) : checkoutURL setCheckoutLink(buildCheckoutLink) @@ -87,26 +93,26 @@ const Cart = ({ data }) => {
- Your Cart {cartCount} + Votre panier {cartCount}
- {lineItems?.length ? ( - + {cartLineItems?.length ? ( + ) : ( )}
- {lineItems?.length > 0 && ( + {cartLineItems?.length > 0 && (
Subtotal - ${centsToPrice(subTotal)} + {centsToPrice(subTotal * 100)}$
{ return (
{items.map((item) => { - return + return })}
) diff --git a/components/header.js b/components/header.js index 69222e0..ae61984 100644 --- a/components/header.js +++ b/components/header.js @@ -1,25 +1,26 @@ -import React, { useState, useRef, useEffect } from 'react' -import { m } from 'framer-motion' -import FocusTrap from 'focus-trap-react' -import { useInView } from 'react-cool-inview' import { useRect } from '@reach/rect' -import { useRouter } from 'next/router' -import Link from 'next/link' import cx from 'classnames' +import FocusTrap from 'focus-trap-react' +import { m } from 'framer-motion' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useEffect, useRef, useState } from 'react' +import { useInView } from 'react-cool-inview' import { isBrowser } from '@lib/helpers' import { + useCartCount, + useCheckoutCount, useSiteContext, - useToggleMegaNav, useToggleCart, - useCartCount, + useToggleMegaNav, } from '@lib/context' -import PromoBar from '@components/promo-bar' +import Icon from '@components/icon' import Menu from '@components/menu' import MegaNavigation from '@components/menu-mega-nav' -import Icon from '@components/icon' +import PromoBar from '@components/promo-bar' const Header = ({ data = {}, isTransparent, onSetup = () => {} }) => { // expand our header data @@ -216,6 +217,7 @@ const Header = ({ data = {}, isTransparent, onSetup = () => {} }) => { const CartToggle = () => { const toggleCart = useToggleCart() + const checkoutCount = useCheckoutCount() const cartCount = useCartCount() return ( diff --git a/lib/context.js b/lib/context.js index 5f30e32..bee7c4d 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1,5 +1,5 @@ import { Base64 } from 'base64-string' -import { createContext, useContext, useState } from 'react' +import { createContext, useContext, useEffect, useState } from 'react' // get our API clients (shopify + sanity) import { getSanityClient } from '@lib/sanity' @@ -35,7 +35,8 @@ const SiteContext = createContext({ }) // set Shopify variables -const shopifyCartID = 'shopify_checkout_id' +const shopifyCheckoutID = 'shopify_checkout_id' +const shopifyCartID = 'shopify_cart_id' const shopifyVariantGID = 'gid://shopify/ProductVariant/' // Set ShopifyGraphQL as a function so we can reuse it @@ -54,7 +55,7 @@ async function shopifyGraphQL(query, variables) { query: query, variables: variables ?? null, }), - } + }, ) const data = await res.json() @@ -65,7 +66,7 @@ async function shopifyGraphQL(query, variables) { } } // defining what the query returns for the product so that we can easily reuse it -const product = `product { +const graphProduct = `product { title handle images(first: 5) { @@ -79,7 +80,51 @@ const product = `product { } }` +const graphCart = `cart { + id + checkoutUrl + lines(first: 250) { + edges { + node { + id + merchandise { + ... on ProductVariant { + id + ${graphProduct} + } + } + quantity + cost { + amountPerQuantity { + amount + } + compareAtAmountPerQuantity { + amount + } + subtotalAmount { + amount + } + totalAmount { + amount + } + } + } + } + } + cost { + checkoutChargeAmount { + amount + } + } + }` + // Build a new checkout +const createNewCheckout = (context) => { + return context.shopifyClient?.checkout.create({ + presentmentCurrencyCode: 'CAD', + }) +} + const createNewCart = async (context) => { // GraphQL query to create a cart const query = `mutation { @@ -96,47 +141,60 @@ const createNewCart = async (context) => { } }` const queryResponse = await shopifyGraphQL(query) - const cart = queryResponse.data.cartCreate - - // Update our global store states - setCartState(cart) - - return + const cart = queryResponse.data.cartCreate.cart + // NEED TO ADD CART TO CONTEXT BUT HOW—seems that L322 is doing the trick + return cart } // Get Shopify checkout cart -const fetchCart = async (context, id) => { +const fetchCheckout = (context, id) => { + return context.shopifyClient?.checkout.fetch(id) +} + +const fetchCart = async (id) => { // GraphQL query to fetch a cart const query = `{ cart(id: "${id}") { id - checkoutUrl - lines(first: 250) { - edges { - node { - id - merchandise { - ... on ProductVariant { - id - ${product} + checkoutUrl + lines(first: 250) { + edges { + node { + id + merchandise { + ... on ProductVariant { + id + ${graphProduct} + } + } + quantity + cost { + amountPerQuantity { + amount + } + compareAtAmountPerQuantity { + amount + } + subtotalAmount { + amount } + totalAmount { + amount + } + } } - quantity } } - } - cost { - checkoutChargeAmount { - amount + cost { + checkoutChargeAmount { + amount + } } - } } }` const queryResponse = await shopifyGraphQL(query) - const cart = queryResponse.data - // Update our global store states - setCartState(cart) - return + const cart = queryResponse.data.cart + return cart } // get associated variant from Sanity @@ -165,38 +223,29 @@ const fetchVariant = async (id) => { value } } - ` + `, ) return variant } // set our checkout states -const setCartState = async (cart, checkout, setContext, openCart) => { - if (!checkout && !cart) return null +const setCheckoutState = async (checkout, setContext, openCart) => { + if (!checkout) return null if (typeof window !== `undefined`) { - localStorage.setItem(shopifyCartID, cart.id) + localStorage.setItem(shopifyCheckoutID, checkout.id) } // get real lineItems data from Sanity - const lineItems = cart?.lines - ? await Promise.all( - cart.lines.edges.map(async (edge) => { - const variantID = edge.node.merchandise.id.replace( - shopifyVariantGID, - '' - ) - const variant = await fetchVariant(variantID) - - return { - ...variant, - quantity: edge.node.quantity, - lineID: edge.node.id, - } - }) - ) - : [] + const lineItems = await Promise.all( + checkout.lineItems.map(async (item) => { + const variantID = item.variant.id.split(shopifyVariantGID)[1] + const variant = await fetchVariant(variantID) + + return { ...variant, quantity: item.quantity, lineID: item.id } + }), + ) // update state setContext((prevState) => { @@ -208,8 +257,43 @@ const setCartState = async (cart, checkout, setContext, openCart) => { isCartOpen: openCart ? true : prevState.isCartOpen, checkout: { id: checkout.id, + lineItems: lineItems, + subTotal: checkout.lineItemsSubtotalPrice, webUrl: checkout.webUrl, }, + } + }) +} + +// set our cart states +const setCartState = async (cart, setContext) => { + if (!cart) return null + + if (typeof window !== `undefined`) { + localStorage.setItem(shopifyCartID, cart.id) + localStorage.setItem(cart, JSON.stringify(cart)) + } + + /* + // get real lineItems data from Sanity + const lineItems = await Promise.all( + cart.lines.edges.map(async (edge) => { + const variantID = edge.node.merchandise.id.replace(shopifyVariantGID, ''); + const variant = await fetchVariant(variantID); + + return { ...variant, quantity: item.quantity, lineID: item.id }; + }), + ); + */ + + // update state + setContext((prevState) => { + return { + ...prevState, + isAdding: false, + isLoading: false, + isUpdating: false, + isCartOpen: true, cart: cart, } }) @@ -229,53 +313,84 @@ const SiteContextProvider = ({ data, children }) => { const [initContext, setInitContext] = useState(false) - /* Is the error here? - useEffect(() => { - // Shopify checkout not build yet - if (initContext === false) { - const initializeCheckout = async () => { - const existingCartID = - typeof window !== 'undefined' - ? localStorage.getItem(shopifyCartID) - : false; - - // existing Shopify checkout ID found - if (existingCartID) { - try { - // fetch checkout from Shopify - const existingCart = await fetchCart(context, existingCartID); - - // /* Disabling for now - // Check if there are invalid items - if ( - existingCheckout.lineItems.some((lineItem) => !lineItem.variant) - ) { - throw new Error( - 'Invalid item in checkout. This variant was probably deleted from Shopify.', - ); - } - - // Make sure this cart hasn’t already been purchased. - if (!existingCheckout.completedAt) { - setCartState(newCart, existingCheckout, setContext); - return; - } - // *close comment - } catch (e) { - localStorage.setItem(shopifyCartID, null); - } - } - - // Otherwise, create a new checkout! - const newCart = await createNewCart(context); - }; - - // Initialize the store context - initializeCheckout(); - setInitContext(true); - } - }, [initContext, context, setContext, context.shopifyClient?.checkout]); // Needs reviewing - */ + useEffect(() => { + // Shopify checkout not build yet + if (initContext === false) { + const initializeCheckout = async () => { + const existingCheckoutID = + typeof window !== 'undefined' + ? localStorage.getItem(shopifyCheckoutID) + : false + + // existing Shopify checkout ID found + if (existingCheckoutID) { + try { + // fetch checkout from Shopify + const existingCheckout = await fetchCheckout( + context, + existingCheckoutID, + ) + + // Check if there are invalid items + if ( + existingCheckout.lineItems.some((lineItem) => !lineItem.variant) + ) { + throw new Error( + 'Invalid item in checkout. This variant was probably deleted from Shopify.', + ) + } + + // Make sure this cart hasn’t already been purchased. + if (!existingCheckout.completedAt) { + setCheckoutState(existingCheckout, setContext) + return + } + } catch (e) { + localStorage.setItem(shopifyCheckoutID, null) + } + } + + // Otherwise, create a new checkout! + const newCheckout = await createNewCheckout(context) + setCheckoutState(newCheckout, setContext) + } + + const initializeCart = async () => { + const existingCartID = localStorage.getItem(shopifyCartID) + //('The existing cart id'); + //console.log(existingCartID); + + // existing Shopify checkout ID found + if (existingCartID != 'null') { + console.log('SiteContextProvider: Fetching our existing cart') + try { + // fetch checkout from Shopify + const existingCart = await fetchCart(existingCartID) + //console.log(existingCart); + setCartState(existingCart, setContext) + } catch (e) { + console.log( + `Couldn't fetch the existing cart... Creating a new one...`, + ) + const newCart = await createNewCart(context) + //console.log(newCart); + setCartState(newCart, setContext) + } + } else { + // Otherwise, create a new checkout! + console.log('SiteContextProvider: Creating a new cart') + const newCart = await createNewCart(context) + //console.log(newCart); + setCartState(newCart, setContext) + } + } + + // Initialize the store context + initializeCheckout() + initializeCart() + setInitContext(true) + } + }, [initContext, context, setContext, context.shopifyClient?.checkout]) return ( item.quantity + total, 0) + } + + return count +} + function useCartCount() { const { context: { cart }, } = useContext(SiteContext) let count = 0 - - if (cart.lines) { - count = cart.lines.edges.forEach((edge) => { - count += edge.node.quantity - }) + if (cart?.lines) { + for (let i = 0; i < cart.lines.edges.length; i++) { + count += cart.lines.edges[i].node.quantity + } } return count } // Access our cart totals +function useCheckoutTotals() { + const { + context: { checkout }, + } = useContext(SiteContext) + + const subTotal = checkout.subTotal ? checkout.subTotal.amount * 100 : false + return { + subTotal, + } +} + function useCartTotals() { const { context: { cart }, @@ -374,6 +513,15 @@ function useCartTotals() { } } +// Access our cart items +function useCheckoutItems() { + const { + context: { checkout }, + } = useContext(SiteContext) + + return checkout.lineItems +} + // Access our cart items function useCartItems() { const { @@ -386,7 +534,7 @@ function useCartItems() { // Add an item to the checkout cart function useAddItem() { const { - context: { cart }, + context: { cart, checkout, shopifyClient }, setContext, } = useContext(SiteContext) @@ -403,6 +551,25 @@ function useAddItem() { const enc = new Base64() const variant = enc.urlEncode(`${shopifyVariantGID}${variantID}`) + // Build the cart line item + const newItem = { + variantId: variant, + quantity: quantity, + customAttributes: attributes, + } + + /* + // Add it to the Shopify checkout cart + const newCheckout = await shopifyClient.checkout.addLineItems( + checkout.id, + newItem, + ); + */ + + // We check if the context is providing a cart.id otherwise we rely on the localStorage we set up. + // Definitely ugly but I can't figure out how to update the context for cart + cart.id == undefined ? localStorage.getItem(shopifyCartID) : cart.id + // GraphQL query to add items to the cart const query = `mutation { cartLinesAdd( @@ -414,29 +581,7 @@ function useAddItem() { } ] ) { - cart { - id - checkoutUrl - lines(first: 250) { - edges { - node { - id - merchandise { - ... on ProductVariant { - id - ${product} - } - } - quantity - } - } - } - cost { - checkoutChargeAmount { - amount - } - } - } + ${graphCart} userErrors { field message @@ -444,12 +589,10 @@ function useAddItem() { } }` const queryResponse = await shopifyGraphQL(query) - const newCart = queryResponse.data.cartLinesAdd - console.log(newCart) - const newCheckout = { webUrl: newCart.checkoutUrl } + const newCart = queryResponse.data.cartLinesAdd.cart - // Update our global store states - setCartState(newCart, newCheckout, setContext, true) + //setCheckoutState(newCheckout, setContext, true); + setCartState(newCart, setContext, true) } return addItem @@ -458,7 +601,7 @@ function useAddItem() { // Update item in cart function useUpdateItem() { const { - context: { cart }, + context: { cart, checkout, shopifyClient }, setContext, } = useContext(SiteContext) @@ -471,6 +614,17 @@ function useUpdateItem() { return { ...prevState, isUpdating: true } }) + /* + const newCheckout = await shopifyClient.checkout.updateLineItems( + checkout.id, + [{ id: itemID, quantity: quantity }], + ); + */ + + // We check if the context is providing a cart.id otherwise we rely on the localStorage we set up. + // Definitely ugly but I can't figure out how to update the context for cart + cart.id == undefined ? localStorage.getItem(shopifyCartID) : cart.id + // GraphQL query to update items in the cart const query = `mutation { cartLinesUpdate( @@ -482,29 +636,7 @@ function useUpdateItem() { }, ] ) { - cart { - id - checkoutUrl - lines(first: 250) { - edges { - node { - id - merchandise { - ... on ProductVariant { - id - ${product} - } - } - quantity - } - } - } - cost { - checkoutChargeAmount { - amount - } - } - } + ${graphCart} userErrors { field message @@ -512,12 +644,10 @@ function useUpdateItem() { } }` const queryResponse = await shopifyGraphQL(query) - const newCart = queryResponse.data.cartLinesUpdate - console.log(newCart) - const newCheckout = { webUrl: newCart.checkoutUrl } + const newCart = queryResponse.data.cartLinesUpdate.cart - // Update our global store states - setCartState(newCart, newCheckout, setContext, true) + //setCheckoutState(newCheckout, setContext); + setCartState(newCart, setContext) } return updateItem } @@ -525,7 +655,7 @@ function useUpdateItem() { // Remove item from cart function useRemoveItem() { const { - context: { cart }, + context: { cart, heckout, shopifyClient }, setContext, } = useContext(SiteContext) @@ -538,6 +668,17 @@ function useRemoveItem() { return { ...prevState, isUpdating: true } }) + /* + const newCheckout = await shopifyClient.checkout.removeLineItems( + checkout.id, + [itemID], + ); + */ + + // We check if the context is providing a cart.id otherwise we rely on the localStorage we set up. + // Definitely ugly but I can't figure out how to update the context for cart + cart.id == undefined ? localStorage.getItem(shopifyCartID) : cart.id + // GraphQL query to remove items from the cart const query = `mutation { cartLinesRemove( @@ -546,29 +687,7 @@ function useRemoveItem() { "${itemID}" ] ) { - cart { - id - checkoutUrl - lines(first: 250) { - edges { - node { - id - merchandise { - ... on ProductVariant { - id - ${product} - } - } - quantity - } - } - } - cost { - checkoutChargeAmount { - amount - } - } - } + ${graphCart} userErrors { field message @@ -576,11 +695,10 @@ function useRemoveItem() { } }` const queryResponse = await shopifyGraphQL(query) - const newCart = queryResponse.data.cartLinesRemove - console.log(newCart) - const newCheckout = { webUrl: newCart.checkoutUrl } + const newCart = queryResponse.data.cartLinesRemove.cart - setCartState(newCart, newCheckout, setContext) + //setCheckoutState(newCheckout, setContext); + setCartState(newCart, setContext) } return removeItem } @@ -588,10 +706,10 @@ function useRemoveItem() { // Build our Checkout URL function useCheckout() { const { - context: { checkout }, + context: { cart, checkout }, } = useContext(SiteContext) - return checkout.webUrl + return cart.checkoutUrl } // Toggle cart state @@ -624,18 +742,20 @@ function useProductCount() { } export { - SiteContextProvider, - useAddItem, - useCartCount, - useCartItems, - useCartTotals, - useCheckout, - useProductCount, - useRemoveItem, - useSiteContext, - useToggleCart, - useToggleMegaNav, - useTogglePageTransition, - useUpdateItem + SiteContextProvider, + useAddItem, + useCartCount, + useCartItems, + useCartTotals, + useCheckout, + useCheckoutCount, + useCheckoutItems, + useCheckoutTotals, + useProductCount, + useRemoveItem, + useSiteContext, + useToggleCart, + useToggleMegaNav, + useTogglePageTransition, + useUpdateItem, } -