Only create cart / cookie when needed

This commit is contained in:
Michael Novotny 2023-07-14 10:57:32 -05:00
parent 37b436af52
commit c2ff3a0f87
No known key found for this signature in database
6 changed files with 31 additions and 80 deletions

View File

@ -1,13 +1,24 @@
'use server'; 'use server';
import { addToCart, removeFromCart, updateCart } from 'lib/shopify'; import { addToCart, createCart, getCart, removeFromCart, updateCart } from 'lib/shopify';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
export const addItem = async (variantId: string | undefined): Promise<Error | undefined> => { export const addItem = async (variantId: string | undefined): Promise<Error | undefined> => {
const cartId = cookies().get('cartId')?.value; let cartId = cookies().get('cartId')?.value;
let cart;
if (!cartId || !variantId) { if (cartId) {
return new Error('Missing cartId or variantId'); cart = await getCart(cartId);
}
if (!cartId || !cart) {
cart = await createCart();
cartId = cart.id;
cookies().set('cartId', cartId);
}
if (!variantId) {
return new Error('Missing variantId');
} }
try { try {
await addToCart(cartId, [{ merchandiseId: variantId, quantity: 1 }]); await addToCart(cartId, [{ merchandiseId: variantId, quantity: 1 }]);

View File

@ -1,23 +1,14 @@
import { createCart, getCart } from 'lib/shopify'; import { getCart } from 'lib/shopify';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import CartModal from './modal'; import CartModal from './modal';
export default async function Cart() { export default async function Cart() {
const cartId = cookies().get('cartId')?.value; const cartId = cookies().get('cartId')?.value;
let cartIdUpdated = false;
let cart; let cart;
if (cartId) { if (cartId) {
cart = await getCart(cartId); cart = await getCart(cartId);
} }
// If the `cartId` from the cookie is not set or the cart is empty return <CartModal cart={cart} />;
// (old carts becomes `null` when you checkout), then get a new `cartId`
// and re-fetch the cart.
if (!cartId || !cart) {
cart = await createCart();
cartIdUpdated = true;
}
return <CartModal cart={cart} cartIdUpdated={cartIdUpdated} />;
} }

View File

@ -9,7 +9,6 @@ import { createUrl } from 'lib/utils';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { Fragment, useEffect, useRef, useState } from 'react'; import { Fragment, useEffect, useRef, useState } from 'react';
import { useCookies } from 'react-cookie';
import CloseCart from './close-cart'; import CloseCart from './close-cart';
import DeleteItemButton from './delete-item-button'; import DeleteItemButton from './delete-item-button';
import EditItemQuantityButton from './edit-item-quantity-button'; import EditItemQuantityButton from './edit-item-quantity-button';
@ -19,27 +18,15 @@ type MerchandiseSearchParams = {
[key: string]: string; [key: string]: string;
}; };
export default function CartModal({ cart, cartIdUpdated }: { cart: Cart; cartIdUpdated: boolean }) { export default function CartModal({ cart }: { cart: Cart | undefined }) {
const [, setCookie] = useCookies(['cartId']);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const quantityRef = useRef(cart.totalQuantity); const quantityRef = useRef(cart?.totalQuantity);
const openCart = () => setIsOpen(true); const openCart = () => setIsOpen(true);
const closeCart = () => setIsOpen(false); const closeCart = () => setIsOpen(false);
useEffect(() => {
if (cartIdUpdated) {
setCookie('cartId', cart.id, {
path: '/',
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production'
});
}
return;
}, [setCookie, cartIdUpdated, cart.id]);
useEffect(() => { useEffect(() => {
// Open cart modal when when quantity changes. // Open cart modal when when quantity changes.
if (cart.totalQuantity !== quantityRef.current) { if (cart && cart.totalQuantity !== quantityRef.current) {
// But only if it's not already open (quantity also changes when editing items in cart). // But only if it's not already open (quantity also changes when editing items in cart).
if (!isOpen) { if (!isOpen) {
setIsOpen(true); setIsOpen(true);
@ -48,12 +35,12 @@ export default function CartModal({ cart, cartIdUpdated }: { cart: Cart; cartIdU
// Always update the quantity reference // Always update the quantity reference
quantityRef.current = cart.totalQuantity; quantityRef.current = cart.totalQuantity;
} }
}, [isOpen, cart.totalQuantity, quantityRef]); }, [isOpen, cart?.totalQuantity, quantityRef]);
return ( return (
<> <>
<button aria-label="Open cart" onClick={openCart} data-testid="open-cart"> <button aria-label="Open cart" onClick={openCart} data-testid="open-cart">
<OpenCart quantity={cart.totalQuantity} /> <OpenCart quantity={cart?.totalQuantity} />
</button> </button>
<Transition show={isOpen}> <Transition show={isOpen}>
<Dialog onClose={closeCart} className="relative z-50" data-testid="cart"> <Dialog onClose={closeCart} className="relative z-50" data-testid="cart">
@ -86,7 +73,7 @@ export default function CartModal({ cart, cartIdUpdated }: { cart: Cart; cartIdU
</button> </button>
</div> </div>
{cart.lines.length === 0 ? ( {!cart || cart.lines.length === 0 ? (
<div className="mt-20 flex w-full flex-col items-center justify-center overflow-hidden"> <div className="mt-20 flex w-full flex-col items-center justify-center overflow-hidden">
<ShoppingCartIcon className="h-16" /> <ShoppingCartIcon className="h-16" />
<p className="mt-6 text-center text-2xl font-bold">Your cart is empty.</p> <p className="mt-6 text-center text-2xl font-bold">Your cart is empty.</p>

View File

@ -234,15 +234,16 @@ export async function updateCart(
return reshapeCart(res.body.data.cartLinesUpdate.cart); return reshapeCart(res.body.data.cartLinesUpdate.cart);
} }
export async function getCart(cartId: string): Promise<Cart | null> { export async function getCart(cartId: string): Promise<Cart | undefined> {
const res = await shopifyFetch<ShopifyCartOperation>({ const res = await shopifyFetch<ShopifyCartOperation>({
query: getCartQuery, query: getCartQuery,
variables: { cartId }, variables: { cartId },
cache: 'no-store' cache: 'no-store'
}); });
// Old carts becomes `null` when you checkout.
if (!res.body.data.cart) { if (!res.body.data.cart) {
return null; return undefined;
} }
return reshapeCart(res.body.data.cart); return reshapeCart(res.body.data.cart);

View File

@ -28,7 +28,6 @@
"clsx": "^1.2.1", "clsx": "^1.2.1",
"next": "13.4.9-canary.2", "next": "13.4.9-canary.2",
"react": "18.2.0", "react": "18.2.0",
"react-cookie": "^4.1.1",
"react-dom": "18.2.0" "react-dom": "18.2.0"
}, },
"devDependencies": { "devDependencies": {

48
pnpm-lock.yaml generated
View File

@ -16,9 +16,6 @@ dependencies:
react: react:
specifier: 18.2.0 specifier: 18.2.0
version: 18.2.0 version: 18.2.0
react-cookie:
specifier: ^4.1.1
version: 4.1.1(react@18.2.0)
react-dom: react-dom:
specifier: 18.2.0 specifier: 18.2.0
version: 18.2.0(react@18.2.0) version: 18.2.0(react@18.2.0)
@ -384,17 +381,6 @@ packages:
tailwindcss: 3.3.2 tailwindcss: 3.3.2
dev: true dev: true
/@types/cookie@0.3.3:
resolution: {integrity: sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==}
dev: false
/@types/hoist-non-react-statics@3.3.1:
resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==}
dependencies:
'@types/react': 18.2.14
hoist-non-react-statics: 3.3.2
dev: false
/@types/json5@0.0.29: /@types/json5@0.0.29:
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
dev: true dev: true
@ -409,6 +395,7 @@ packages:
/@types/prop-types@15.7.5: /@types/prop-types@15.7.5:
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
dev: true
/@types/react-dom@18.2.6: /@types/react-dom@18.2.6:
resolution: {integrity: sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==} resolution: {integrity: sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==}
@ -422,9 +409,11 @@ packages:
'@types/prop-types': 15.7.5 '@types/prop-types': 15.7.5
'@types/scheduler': 0.16.3 '@types/scheduler': 0.16.3
csstype: 3.1.2 csstype: 3.1.2
dev: true
/@types/scheduler@0.16.3: /@types/scheduler@0.16.3:
resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==}
dev: true
/@typescript-eslint/parser@5.61.0(eslint@8.44.0)(typescript@5.1.3): /@typescript-eslint/parser@5.61.0(eslint@8.44.0)(typescript@5.1.3):
resolution: {integrity: sha512-yGr4Sgyh8uO6fSi9hw3jAFXNBHbCtKKFMdX2IkT3ZqpKmtAq3lHS4ixB/COFuAIJpwl9/AqF7j72ZDWYKmIfvg==} resolution: {integrity: sha512-yGr4Sgyh8uO6fSi9hw3jAFXNBHbCtKKFMdX2IkT3ZqpKmtAq3lHS4ixB/COFuAIJpwl9/AqF7j72ZDWYKmIfvg==}
@ -890,11 +879,6 @@ packages:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true dev: true
/cookie@0.4.2:
resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
engines: {node: '>= 0.6'}
dev: false
/cross-spawn@7.0.3: /cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -912,6 +896,7 @@ packages:
/csstype@3.1.2: /csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
dev: true
/damerau-levenshtein@1.0.8: /damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
@ -1740,12 +1725,6 @@ packages:
function-bind: 1.1.1 function-bind: 1.1.1
dev: true dev: true
/hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
dependencies:
react-is: 16.13.1
dev: false
/hosted-git-info@2.8.9: /hosted-git-info@2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
dev: true dev: true
@ -2711,17 +2690,6 @@ packages:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true dev: true
/react-cookie@4.1.1(react@18.2.0):
resolution: {integrity: sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A==}
peerDependencies:
react: '>= 16.3.0'
dependencies:
'@types/hoist-non-react-statics': 3.3.1
hoist-non-react-statics: 3.3.2
react: 18.2.0
universal-cookie: 4.0.4
dev: false
/react-dom@18.2.0(react@18.2.0): /react-dom@18.2.0(react@18.2.0):
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
peerDependencies: peerDependencies:
@ -2734,6 +2702,7 @@ packages:
/react-is@16.13.1: /react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: true
/react@18.2.0: /react@18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
@ -3308,13 +3277,6 @@ packages:
which-boxed-primitive: 1.0.2 which-boxed-primitive: 1.0.2
dev: true dev: true
/universal-cookie@4.0.4:
resolution: {integrity: sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==}
dependencies:
'@types/cookie': 0.3.3
cookie: 0.4.2
dev: false
/untildify@4.0.0: /untildify@4.0.0:
resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
engines: {node: '>=8'} engines: {node: '>=8'}