Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes cart issues #971

Merged
merged 1 commit into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ Looking for Next.js Commerce v1? [View the release notes](https://github.com/ver
2. Link local instance with Vercel and Github accounts (creates .vercel file): `vercel link`
3. Download your environment variables: `vercel env pull .env.local`


```bash
pnpm install
pnpm dev
Expand Down
14 changes: 9 additions & 5 deletions components/cart/delete-item-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import LoadingDots from 'components/loading-dots';
import { useRouter } from 'next/navigation';
import { startTransition, useState } from 'react';

import clsx from 'clsx';
import type { CartItem } from 'lib/shopify/types';

export default function DeleteItemButton({ item }: { item: CartItem }) {
Expand Down Expand Up @@ -36,14 +37,17 @@ export default function DeleteItemButton({ item }: { item: CartItem }) {
aria-label="Remove cart item"
onClick={handleRemove}
disabled={removing}
className={`${
removing ? 'cursor-not-allowed' : ''
} mr-2 flex h-8 w-8 items-center justify-center border border-black/40 bg-black/0 hover:bg-black/10 dark:border-white/40 dark:bg-white/0 dark:hover:bg-white/10`}
className={clsx(
'ease flex min-w-[36px] max-w-[36px] items-center justify-center border px-2 transition-all duration-200 hover:border-gray-800 hover:bg-gray-100 dark:border-gray-700 dark:hover:border-gray-600 dark:hover:bg-gray-900',
{
'cursor-not-allowed px-0': removing
}
)}
>
{removing ? (
<LoadingDots className="bg-white dark:bg-black" />
<LoadingDots className="bg-black dark:bg-white" />
) : (
<CloseIcon className="hover:text-accent-3 h-6" />
<CloseIcon className="hover:text-accent-3 mx-[1px] h-4 w-4" />
)}
</button>
);
Expand Down
17 changes: 11 additions & 6 deletions components/cart/edit-item-quantity-button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useRouter } from 'next/navigation';
import { startTransition, useState } from 'react';

import clsx from 'clsx';
import MinusIcon from 'components/icons/minus';
import PlusIcon from 'components/icons/plus';
import type { CartItem } from 'lib/shopify/types';
Expand Down Expand Up @@ -46,16 +47,20 @@ export default function EditItemQuantityButton({
aria-label={type === 'plus' ? 'Increase item quantity' : 'Reduce item quantity'}
onClick={handleEdit}
disabled={editing}
className={`${editing ? 'cursor-not-allowed' : ''} ${
type === 'minus' ? 'ml-auto' : ''
} flex h-8 w-8 items-center justify-center border-l border-black/40 bg-black/0 hover:bg-black/10 dark:border-white/40 dark:bg-white/0 dark:hover:bg-white/10`}
className={clsx(
'ease flex min-w-[36px] max-w-[36px] items-center justify-center border px-2 transition-all duration-200 hover:border-gray-800 hover:bg-gray-100 dark:border-gray-700 dark:hover:border-gray-600 dark:hover:bg-gray-900',
{
'cursor-not-allowed': editing,
'ml-auto': type === 'minus'
}
)}
>
{editing ? (
<LoadingDots className="bg-white dark:bg-black" />
<LoadingDots className="bg-black dark:bg-white" />
) : type === 'plus' ? (
<PlusIcon className="h-4" />
<PlusIcon className="h-4 w-4" />
) : (
<MinusIcon className="h-4" />
<MinusIcon className="h-4 w-4" />
)}
</button>
);
Expand Down
169 changes: 91 additions & 78 deletions components/cart/modal.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { Dialog } from '@headlessui/react';
import { AnimatePresence, motion } from 'framer-motion';
import Image from 'next/image';
import Link from 'next/link';

import CloseIcon from 'components/icons/close';
import ShoppingBagIcon from 'components/icons/shopping-bag';
import Price from 'components/price';
import { DEFAULT_OPTION } from 'lib/constants';
import type { Cart } from 'lib/shopify/types';
import { createUrl } from 'lib/utils';
import DeleteItemButton from './delete-item-button';
import EditItemQuantityButton from './edit-item-quantity-button';

type MerchandiseSearchParams = {
[key: string]: string;
};

export default function CartModal({
isOpen,
onClose,
Expand Down Expand Up @@ -50,7 +56,7 @@ export default function CartModal({
closed: { translateX: '100%' }
}}
transition={{ type: 'spring', bounce: 0, duration: 0.3 }}
className="flex w-full flex-col bg-white p-8 text-black dark:bg-black dark:text-white md:w-1/3 lg:w-[30%] lg:px-6"
className="flex w-full flex-col bg-white p-8 text-black dark:bg-black dark:text-white md:w-3/5 lg:w-2/5"
>
<div className="flex items-center justify-between">
<p className="text-lg font-bold">My Cart</p>
Expand All @@ -74,95 +80,102 @@ export default function CartModal({
<div className="flex h-full flex-col justify-between overflow-hidden">
<ul className="flex-grow overflow-auto p-6">
{cart.lines.map((item, i) => {
const merchandiseSearchParams = {} as MerchandiseSearchParams;

item.merchandise.selectedOptions.forEach(({ name, value }) => {
if (value !== DEFAULT_OPTION) {
merchandiseSearchParams[name.toLowerCase()] = value;
}
});

const merchandiseUrl = createUrl(
`/product/${item.merchandise.product.handle}`,
new URLSearchParams(merchandiseSearchParams)
);

return (
<li key={i} data-testid="cart-item">
<div className="mb-2 flex w-full">
<div className="relative h-20 w-20 flex-none">
{item.merchandise.product.featuredImage.url && (
<Image
alt={
item.merchandise.product.featuredImage.altText ||
item.merchandise.product.title
}
className="bg-white"
fill
src={item.merchandise.product.featuredImage.url}
/>
)}
<Link
className="flex flex-row space-x-4 py-4"
href={merchandiseUrl}
onClick={onClose}
>
<div className="relative h-16 w-16 cursor-pointer overflow-hidden bg-white">
<Image
className="h-full w-full object-cover"
width={64}
height={64}
alt={
item.merchandise.product.featuredImage.altText ||
item.merchandise.product.title
}
src={item.merchandise.product.featuredImage.url}
/>
</div>
<div className="ml-4 flex w-full flex-col justify-between">
<div className="flex w-full justify-between">
<div>
<p
className="text-lg font-medium"
data-testid="cart-product-name"
>
{item.merchandise.product.title}
</p>
{item.merchandise.title !== DEFAULT_OPTION ? (
<p className="text-sm" data-testid="cart-product-variant">
{item.merchandise.title}
</p>
) : null}
</div>
<Price
className="font-medium"
amount={item.cost.totalAmount.amount}
currencyCode={item.cost.totalAmount.currencyCode}
/>
</div>
<div className="flex flex-1 flex-col text-base">
<span className="font-semibold">
{item.merchandise.product.title}
</span>
{item.merchandise.title !== DEFAULT_OPTION ? (
<p className="text-sm" data-testid="cart-product-variant">
{item.merchandise.title}
</p>
) : null}
</div>
</div>
<div className="mb-4 flex w-full">
<Price
className="flex flex-col justify-between space-y-2 text-sm"
amount={item.cost.totalAmount.amount}
currencyCode={item.cost.totalAmount.currencyCode}
/>
</Link>
<div className="flex h-9 flex-row">
<DeleteItemButton item={item} />
<div className="flex h-8 w-full border border-black/40 dark:border-white/40">
<div className="flex h-full items-center px-2 ">{item.quantity}</div>
<EditItemQuantityButton item={item} type="minus" />
<EditItemQuantityButton item={item} type="plus" />
</div>
<p className="ml-2 flex w-full items-center justify-center border dark:border-gray-700">
<span className="w-full px-2">{item.quantity}</span>
</p>
<EditItemQuantityButton item={item} type="minus" />
<EditItemQuantityButton item={item} type="plus" />
</div>
</li>
);
})}
</ul>
<div className="border-t border-white/60 p-6">
<div className="text-sm text-white">
<div className="mb-2 flex items-center justify-between">
<p>Subtotal</p>
<Price
className="text-right"
amount={cart.cost.subtotalAmount.amount}
currencyCode={cart.cost.subtotalAmount.currencyCode}
/>
</div>
<div className="mb-2 flex items-center justify-between">
<p>Taxes</p>
<Price
className="text-right"
amount={cart.cost.totalTaxAmount.amount}
currencyCode={cart.cost.totalTaxAmount.currencyCode}
/>
</div>
<div className="mb-2 flex items-center justify-between border-b border-white/30 pb-2">
<p>Shipping</p>
<p className="text-right uppercase">calculated at checkout</p>
</div>
<div className="mb-2 flex items-center justify-between font-bold">
<p>Total</p>
<Price
className="text-right"
amount={cart.cost.totalAmount.amount}
currencyCode={cart.cost.totalAmount.currencyCode}
/>
</div>
<div className="border-t border-gray-200 pt-2 text-sm text-black dark:text-white">
<div className="mb-2 flex items-center justify-between">
<p>Subtotal</p>
<Price
className="text-right"
amount={cart.cost.subtotalAmount.amount}
currencyCode={cart.cost.subtotalAmount.currencyCode}
/>
</div>
<div className="mb-2 flex items-center justify-between">
<p>Taxes</p>
<Price
className="text-right"
amount={cart.cost.totalTaxAmount.amount}
currencyCode={cart.cost.totalTaxAmount.currencyCode}
/>
</div>
<div className="mb-2 flex items-center justify-between border-b border-gray-200 pb-2">
<p>Shipping</p>
<p className="text-right">Calculated at checkout</p>
</div>
<div className="mb-2 flex items-center justify-between font-bold">
<p>Total</p>
<Price
className="text-right"
amount={cart.cost.totalAmount.amount}
currencyCode={cart.cost.totalAmount.currencyCode}
/>
</div>
<a
href={cart.checkoutUrl}
className="mt-6 flex w-full items-center justify-center bg-black p-3 text-sm font-medium uppercase text-white opacity-90 hover:opacity-100 dark:bg-white dark:text-black"
>
<span>Proceed to Checkout</span>
</a>
</div>
<a
href={cart.checkoutUrl}
className="flex w-full items-center justify-center bg-black p-3 text-sm font-medium uppercase text-white opacity-90 hover:opacity-100 dark:bg-white dark:text-black"
>
<span>Proceed to Checkout</span>
</a>
</div>
) : null}
</Dialog.Panel>
Expand Down
4 changes: 4 additions & 0 deletions lib/shopify/fragments/cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ const cartFragment = /* GraphQL */ `
... on ProductVariant {
id
title
selectedOptions {
name
value
}
product {
...product
}
Expand Down
4 changes: 2 additions & 2 deletions lib/shopify/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ import {
ShopifyUpdateCartOperation
} from './types';

const domain = process.env.SHOPIFY_STORE_DOMAIN!;
const endpoint = process.env.SHOPIFY_STORE_DOMAIN! + SHOPIFY_GRAPHQL_API_ENDPOINT;
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> = T extends { variables: object } ? T['variables'] : never;
Expand Down
4 changes: 4 additions & 0 deletions lib/shopify/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export type CartItem = {
merchandise: {
id: string;
title: string;
selectedOptions: {
name: string;
value: string;
}[];
product: Product;
};
};
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"clsx": "^1.2.1",
"framer-motion": "^8.4.0",
"is-empty-iterable": "^3.0.0",
"next": "13.3.1-canary.7",
"next": "13.3.1-canary.13",
"react": "18.2.0",
"react-cookie": "^4.1.1",
"react-dom": "18.2.0"
Expand Down
Loading