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 && }
-
{item.title}
+ {/*
+ {item.merchandise.product.title} +
*/}

toggleCart(false)} className="cart-item--link" > - {item.product.title} + {item.merchandise.product.title}

- +
@@ -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 7fb8ef1..567f501 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1,5 +1,5 @@ -import React, { createContext, useContext, useEffect, useState } from 'react' import { Base64 } from 'base64-string' +import { createContext, useContext, useEffect, useState } from 'react' // get our API clients (shopify + sanity) import { getSanityClient } from '@lib/sanity' @@ -25,6 +25,7 @@ const initialContext = { id: null, lineItems: [], }, + cart: {}, } // Set context @@ -33,18 +34,169 @@ const SiteContext = createContext({ setContext: () => null, }) +// set Shopify variables +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 +async function shopifyGraphQL(query, variables) { + try { + const res = await fetch( + `https://${process.env.NEXT_PUBLIC_SHOPIFY_STORE_ID}.myshopify.com/api/2023-10/graphql.json`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Shopify-Storefront-Access-Token': + process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_API_TOKEN, + }, + body: JSON.stringify({ + query: query, + variables: variables ?? null, + }), + }, + ) + + const data = await res.json() + return data + } catch (error) { + console.error(error) + return '' + } +} +// defining what the query returns for the product so that we can easily reuse it +const graphProduct = `product { + title + handle + images(first: 5) { + edges { + node { + id + originalSrc + altText + } + } + } + }` + +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: 'USD', + presentmentCurrencyCode: 'CAD', }) } +const createNewCart = async (context) => { + // GraphQL query to create a cart + const query = `mutation { + cartCreate(input: {lines: []}) { + cart { + id + checkoutUrl + cost { + checkoutChargeAmount { + amount + } + } + } + } +}` + const queryResponse = await shopifyGraphQL(query) + 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 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 + ${graphProduct} + } + } + quantity + cost { + amountPerQuantity { + amount + } + compareAtAmountPerQuantity { + amount + } + subtotalAmount { + amount + } + totalAmount { + amount + } + } + } + } + } + cost { + checkoutChargeAmount { + amount + } + } + } +}` + const queryResponse = await shopifyGraphQL(query) + const cart = queryResponse.data.cart + return cart +} + // get associated variant from Sanity const fetchVariant = async (id) => { const variant = await getSanityClient().fetch( @@ -71,16 +223,12 @@ const fetchVariant = async (id) => { value } } - ` + `, ) return variant } -// set Shopify variables -const shopifyCheckoutID = 'shopify_checkout_id' -const shopifyVariantGID = 'gid://shopify/ProductVariant/' - // set our checkout states const setCheckoutState = async (checkout, setContext, openCart) => { if (!checkout) return null @@ -96,7 +244,7 @@ const setCheckoutState = async (checkout, setContext, openCart) => { const variant = await fetchVariant(variantID) return { ...variant, quantity: item.quantity, lineID: item.id } - }) + }), ) // update state @@ -117,6 +265,40 @@ const setCheckoutState = async (checkout, setContext, openCart) => { }) } +// set our cart states +const setCartState = async (cart, setContext, openCart) => { + 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: openCart ? true : prevState.isCartOpen, + cart: cart, + } + }) +} + /* ------------------------------ */ /* Our Context Wrapper /* ------------------------------ */ @@ -146,7 +328,7 @@ const SiteContextProvider = ({ data, children }) => { // fetch checkout from Shopify const existingCheckout = await fetchCheckout( context, - existingCheckoutID + existingCheckoutID, ) // Check if there are invalid items @@ -154,7 +336,7 @@ const SiteContextProvider = ({ data, children }) => { existingCheckout.lineItems.some((lineItem) => !lineItem.variant) ) { throw new Error( - 'Invalid item in checkout. This variant was probably deleted from Shopify.' + 'Invalid item in checkout. This variant was probably deleted from Shopify.', ) } @@ -173,8 +355,39 @@ const SiteContextProvider = ({ data, children }) => { 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]) @@ -238,7 +451,7 @@ function useToggleMegaNav() { /* ------------------------------ */ // Access our cart item count -function useCartCount() { +function useCheckoutCount() { const { context: { checkout }, } = useContext(SiteContext) @@ -252,8 +465,23 @@ function useCartCount() { return count } +function useCartCount() { + const { + context: { cart }, + } = useContext(SiteContext) + + let count = 0 + 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 useCartTotals() { +function useCheckoutTotals() { const { context: { checkout }, } = useContext(SiteContext) @@ -264,8 +492,29 @@ function useCartTotals() { } } +function useCartTotals() { + const { + context: { cart }, + } = useContext(SiteContext) + + // Our GraphQL queries always return this property: + /* + The estimated amount, before taxes and discounts, for the customer to pay at checkout. + The checkout charge amount doesn't include any deferred payments that'll be paid at a later date. + If the cart has no deferred payments, then the checkout charge amount is equivalent to subtotalAmount. + */ + // We might want to use a different key in order to get discount included + // https://shopify.dev/docs/api/storefront/2023-10/objects/CartCost + const subTotal = cart?.cost?.checkoutChargeAmount?.amount + ? cart.cost.checkoutChargeAmount.amount + : false + return { + subTotal, + } +} + // Access our cart items -function useCartItems() { +function useCheckoutItems() { const { context: { checkout }, } = useContext(SiteContext) @@ -273,10 +522,19 @@ function useCartItems() { return checkout.lineItems } +// Access our cart items +function useCartItems() { + const { + context: { cart }, + } = useContext(SiteContext) + + return cart?.lines?.edges +} + // Add an item to the checkout cart function useAddItem() { const { - context: { checkout, shopifyClient }, + context: { cart, checkout, shopifyClient }, setContext, } = useContext(SiteContext) @@ -300,14 +558,41 @@ function useAddItem() { customAttributes: attributes, } - // Add it to the Shopify checkout cart - const newCheckout = await shopifyClient.checkout.addLineItems( - checkout.id, - newItem - ) + /* + // 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( + cartId: "${cart.id}" + lines: [ + { + merchandiseId: "${variant}", + quantity: ${quantity} + } + ] + ) { + ${graphCart} + userErrors { + field + message + } + } +}` + const queryResponse = await shopifyGraphQL(query) + const newCart = queryResponse.data.cartLinesAdd.cart - // Update our global store states - setCheckoutState(newCheckout, setContext, true) + //setCheckoutState(newCheckout, setContext, true); + setCartState(newCart, setContext, true) } return addItem @@ -316,7 +601,7 @@ function useAddItem() { // Update item in cart function useUpdateItem() { const { - context: { checkout, shopifyClient }, + context: { cart, checkout, shopifyClient }, setContext, } = useContext(SiteContext) @@ -329,12 +614,40 @@ function useUpdateItem() { return { ...prevState, isUpdating: true } }) - const newCheckout = await shopifyClient.checkout.updateLineItems( - checkout.id, - [{ id: itemID, quantity: quantity }] - ) + /* + 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( + cartId: "${cart.id}" + lines: [ + { + id: "${itemID}", + quantity: ${quantity} + }, + ] + ) { + ${graphCart} + userErrors { + field + message + } + } +}` + const queryResponse = await shopifyGraphQL(query) + const newCart = queryResponse.data.cartLinesUpdate.cart - setCheckoutState(newCheckout, setContext) + //setCheckoutState(newCheckout, setContext); + setCartState(newCart, setContext) } return updateItem } @@ -342,7 +655,7 @@ function useUpdateItem() { // Remove item from cart function useRemoveItem() { const { - context: { checkout, shopifyClient }, + context: { cart, heckout, shopifyClient }, setContext, } = useContext(SiteContext) @@ -355,12 +668,37 @@ function useRemoveItem() { return { ...prevState, isUpdating: true } }) - const newCheckout = await shopifyClient.checkout.removeLineItems( - checkout.id, - [itemID] - ) + /* + 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( + cartId: "${cart.id}" + lineIds: [ + "${itemID}" + ] + ) { + ${graphCart} + userErrors { + field + message + } + } +}` + const queryResponse = await shopifyGraphQL(query) + const newCart = queryResponse.data.cartLinesRemove.cart - setCheckoutState(newCheckout, setContext) + //setCheckoutState(newCheckout, setContext); + setCartState(newCart, setContext) } return removeItem } @@ -368,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 @@ -405,16 +743,20 @@ function useProductCount() { export { SiteContextProvider, - useSiteContext, - useTogglePageTransition, - useToggleMegaNav, + useAddItem, useCartCount, - useCartTotals, useCartItems, - useAddItem, - useUpdateItem, - useRemoveItem, + useCartTotals, useCheckout, - useToggleCart, + useCheckoutCount, + useCheckoutItems, + useCheckoutTotals, useProductCount, + useRemoveItem, + useSiteContext, + useToggleCart, + useToggleMegaNav, + useTogglePageTransition, + useUpdateItem } +