vercel / commerce

Next.js Commerce
https://demo.vercel.store
MIT License
10.54k stars 3.88k forks source link

Code for Cart Discount #1316

Open osseonews opened 3 months ago

osseonews commented 3 months ago

We developed the code to apply a discount in the cart. I would normally put in a pull request for this type of thing, but our code now has so many conflicts, it would be too complicated. I figured I'd just post the basic code here and hopefully it will help people who are looking to get this functionality. Adapt to your base code, obviously.

  1. Add a Mutation in mutations/cart.ts
export const CART_DISCOUNT_CODE_UPDATE_MUTATION = /* GraphQL */ `
  mutation cartDiscountCodesUpdate ($cartId: ID!
    $discountCodes: [String!]) {
    cartDiscountCodesUpdate(cartId: $cartId, discountCodes: $discountCodes) {
      cart {
        ...cart
      }
    }
  }
  ${cartFragment}
`;
  1. Add a Function in lib/shopify/index where all the functions are:

    export async function updateDiscounts(cartId: string, discounts: string[]): Promise<Cart> {
    const res = await shopifyFetch<ShopifyDiscountCartOperation>({
    query: CART_DISCOUNT_CODE_UPDATE_MUTATION,
    variables: {
      cartId,
      discountCodes: discounts
    },
    cache: 'no-store'
    });
    return reshapeCart(res.body.data.cartDiscountCodesUpdate.cart);
    }
  2. Add an Action in cart/actions.ts

export async function applyDiscount(
  prevState: any,
  formData: FormData
) {
  //console.log ("Form Data", formData)
  const cartId = cookies().get('cartId')?.value;

  if (!cartId) {
    return 'Missing cart ID';
  }
    const schema = z.object({
    discountCode: z.string().min(1),
  });
  const parse = schema.safeParse({
    discountCode: formData.get("discountCode"),
  });

  if (!parse.success) {
    return "Error applying discount. Discount code required.";
  }

  const data = parse.data;
  let discountCodes = []; // Create a new empty array - actually this array should be the current array of discount codes, but as we only allow one code now, we create an empty array
  discountCodes.push(data.discountCode); // Push the string into the array
  // Ensure the discount codes are unique - this is not really necessary now, because we are only using one code
  const uniqueCodes = discountCodes.filter((value, index, array) => {
    return array.indexOf(value) === index;
  });

  try {
    await updateDiscounts(cartId, uniqueCodes);
    //close cart and have tooltip for c
    revalidateTag(TAGS.cart);
  } catch (e) {
    return 'Error applying discount';
  }
}

export async function removeDiscount(
  prevState: any,
  payload: {
    discount: string;
    discounts: string[];
  }
) {
  //console.log ("payload", payload)
  const cartId = cookies().get('cartId')?.value;

  if (!cartId) {
    return 'Missing cart ID';
  }
  const code = payload?.discount
  const codes = payload?.discounts ?? [] //the entire array of discounts
  if (!code) {
    return "Error removing discount. Discount code required.";
  }
  let discountCodes = codes; 
  //remove the code from the array and return the array
  let newCodes = discountCodes.filter(item => item !== code);

  try {
    await updateDiscounts(cartId, newCodes);
    //close cart and have tooltip for c
    revalidateTag(TAGS.cart);
  } catch (e) {
    return 'Error applying discount';
  }
}
  1. Add a Form Component in your Modal for the cart that is something like this (we use Shadcn, so many our components come from there)

    
    function SubmitButton(props: Props) {
    const { pending } = useFormStatus();
    const buttonClasses =
    'mt-3 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-9 rounded-md px-3';
    const disabledClasses = 'cursor-not-allowed opacity-60 hover:opacity-60';
    const message = props?.message;
    return (
    <>
      {message && (
        <Alert className="my-5" variant="destructive">
          <ExclamationTriangleIcon className="mr-2 h-4 w-4" />
          <AlertTitle>{message}</AlertTitle>
        </Alert>
      )}
      {pending ? "" : <input type="text" name="discountCode" placeholder="Discount code" required />}
    
      <button
        onClick={(e: React.FormEvent<HTMLButtonElement>) => {
          if (pending) e.preventDefault();
        }}
        aria-label="Add Discount Code"
        aria-disabled={pending}
        className={clsx(buttonClasses, {
          'hover:opacity-90': true,
        })}
      >
        <div className="w-full">
          {pending ? <LoadingDots className="bg-secondary text-white mb-3" /> : null}
        </div>
        {pending ? '' : 'Apply Discount'}
      </button>
    </>
    );
    }

export function ApplyCartDiscount({ cart }: { cart: Cart }) { const [message, formAction] = useFormState(applyDiscount, null); //console.log('Cart Discount Codes', cart?.discountCodes); //filter the discount codes to remove those where applicablce is false and then jsut get the code in array //if we have a discount code we don't show the form const filteredCodes = cart?.discountCodes && cart?.discountCodes.length ? cart?.discountCodes.filter((code) => code.applicable).map((code) => code.code) : [];

return ( <> {filteredCodes && filteredCodes.length > 0 ? (

{filteredCodes.map((code, index) => ( {code} ))}
  ) : (
    <form action={formAction}>
      <SubmitButton message={message} />
      <p aria-live="polite" className="sr-only" role="status">
        {message}
      </p>
    </form>
  )}
</>

); }

```js
function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button
      type="submit"
      onClick={(e: React.FormEvent<HTMLButtonElement>) => {
        if (pending) e.preventDefault();
      }}
      aria-label="Remove Discount"
      aria-disabled={pending}
      className={clsx(
        'ease flex',
        {
          'cursor-not-allowed px-0': pending
        }
      )}
    >
      {pending ? (
        <LoadingDots className="bg-primary" />
      ) : (
        <XCircleIcon className="mx-5 text-primary hover:border-neutral-800 hover:opacity-80 h-5 w-5" />
      )}
    </button>
  );
}

export function DeleteDiscountButton({ item,items}: { item: string, items:string[]}) {
  const [message, formAction] = useFormState(removeDiscount, null);
  const payload = {
    discount: item,
    discounts: items,
  };
  const actionWithVariant = formAction.bind(null, payload);

  return (
    <form action={actionWithVariant}>
      <SubmitButton />
      <p aria-live="polite" className="sr-only" role="status">
        {message}
      </p>
    </form>
  );
}