Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More

@appmate/wishlist-hydrogen

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@appmate/wishlist-hydrogen

Wishlist King SDK for Shopify Hydrogen


Version published
Maintainers
1
Created

Wishlist King SDK for Shopify Hydrogen

Integrate Wishlist King into your Shopify Hydrogen project.

Install package with npm i @appmate/wishlist-hydrogen.

Create Client

Setup a Wishlist King client in server.ts.

// Create a client
const { wishlistKing } = createWishlistClient({
  shopDomain: "your-domain.myshopify.com",
  // Make sure your session stores the customer id.
  customerId: session.get("customerId"),
  cache: caches.open("wishlist-king"),
  request,
  waitUntil,
});

// Add client to request handler
const handleRequest = createRequestHandler({
  build: remixBuild,
  mode: process.env.NODE_ENV,
  getLoadContext: () => ({
    // Keep your current context and add wishlistKing
    wishlistKing,
  }),
});

Add Components

Create file components/WishlistButton.tsx and copy the blow sample.

import { useMemo } from "react";
import {
  WishlistActionType,
  useWishlistContext,
  useWishlistProduct,
} from "@appmate/wishlist-hydrogen";

export type WishlistButtonProps = {
  productId?: string;
  variantId?: string;
};

const WishlistButton = ({ productId = "", variantId }: WishlistButtonProps) => {
  const wishlist = useWishlistContext();
  const product = useWishlistProduct({
    wishlist,
    productId,
    variantId,
  });

  const inWishlist = useMemo(() => {
    if (
      wishlist.pendingAction?.type === WishlistActionType.ADD_TO_WISHLIST &&
      product.equals(wishlist.pendingAction.payload)
    ) {
      return true;
    }
    if (
      wishlist.pendingAction?.type ===
        WishlistActionType.REMOVE_FROM_WISHLIST &&
      product.equals(wishlist.pendingAction.payload)
    ) {
      return false;
    }

    return product.inWishlist;
  }, [wishlist, product]);

  return (
    <button onClick={product.toggleProduct} disabled={!wishlist.idle}>
      {inWishlist ? "Remove from Wishlist" : "Add to Wishlist"}
    </button>
  );
};

export default WishlistButton;

Create file components/WishlistGrid.tsx and copy the blow sample.

import WishlistButton from "./WishlistButton";

export type WishlistGridProps = {
  products: { id: string; title: string }[];
  onLoadMore: () => void;
  loading: boolean;
  hasNextPage: boolean;
};

const WishlistGrid = ({
  products,
  onLoadMore,
  loading: loadingMore,
  hasNextPage: hasMore,
}: WishlistGridProps) => {
  return (
    <div>
      <div>
        <h1>Wishlist</h1>
        {!products.length ? (
          <p>You wishllist is empty!</p>
        ) : (
          <div>
            {/* TODO: Render your product cards here */}
            {products.map((product) => (
              <div key={product.id}>
                <p>{product.title}</p>
                <WishlistButton productId={product.id} />
              </div>
            ))}
          </div>
        )}
      </div>
      {hasMore && (
        <button onClick={onLoadMore} disabled={loadingMore}>
          Load more
        </button>
      )}
    </div>
  );
};

export default WishlistGrid;

Create file components/WishlistLink.tsx and copy the blow sample.

import { useMemo } from "react";
import { Link } from "@remix-run/react";
import type { LinkProps } from "@remix-run/react";
import {
  WishlistActionType,
  useWishlistContext,
} from "@appmate/wishlist-hydrogen";

export type WishlistLinkProps = Partial<LinkProps>;

const WishlistLink = ({ ...props }: WishlistLinkProps) => {
  const wishlist = useWishlistContext();

  const numItems = useMemo(() => {
    if (wishlist.pendingAction?.type === WishlistActionType.ADD_TO_WISHLIST) {
      return wishlist.data.numItems + 1;
    }
    if (
      wishlist.pendingAction?.type === WishlistActionType.REMOVE_FROM_WISHLIST
    ) {
      return wishlist.data.numItems - 1;
    }
    return wishlist.data.numItems;
  }, [wishlist]);

  return (
    <Link to="/wishlists/mine" {...props}>
      Wishlist ({numItems})
    </Link>
  );
};

export default WishlistLink;

Add Wishlist Provider

Render your main layout inside the WishlistProvider.

<WishlistProvider>{/* Add your layout here */}</WishlistProvider>

Create Wishlist Route

Create file routes/($locale).wishlists.$wishlistHandle.tsx and copy the blow sample.

import { useLoaderData } from "@remix-run/react";
import { ActionArgs, LoaderArgs, json } from "@shopify/remix-oxygen";
import { useMemo } from "react";
import {
  useWishlistContext,
  useLoadMore,
  getProductState,
} from "@appmate/wishlist-hydrogen";
import WishlistGrid from "../components/WishlistGrid";

const PRODUCTS_PER_PAGE = 24;

// TODO: Add your product query required for produt cards.
const PRODUCT_CARDS_QUERY = `
  query ProductCardQuery (
    $productIds:[ID!]!
    $country: CountryCode
    $language: LanguageCode
    ) @inContext(country: $country, language: $language) {
    nodes(ids:$productIds){
      id
      ... on Product {
        title
      }
    }
  }
`;

export interface ProductCardNodes {
  nodes: {
    id: string;
    title: string;
  }[];
}

export async function loader({
  params,
  request,
  context: { storefront, wishlistKing },
}: LoaderArgs) {
  const { wishlistHandle = "mine" } = params;
  const searchParams = new URL(request.url).searchParams;
  const skipProducts = searchParams.get("skip-products");

  const { wishlist } = await wishlistKing.loadWishlist({
    wishlistId: wishlistHandle,
  });

  if (skipProducts) {
    return json({
      wishlist,
      products: [],
      analytics: null,
      pageInfo: {
        hasNextPage: !!wishlist.items.length,
        startCursor: wishlist.items[0]?.id ?? null,
        endCursor: wishlist.items[0]?.id ?? null,
      },
    });
  }

  // Paginate
  const first = parseInt(
    searchParams.get("first") ?? PRODUCTS_PER_PAGE.toString(),
  );
  const after = searchParams.get("after");
  const startIndex = wishlist.items.findIndex((item) => item.id === after) + 1;
  const itemsOnPage = wishlist.items.slice(startIndex, startIndex + first);

  if (!itemsOnPage.length) {
    return json({
      wishlist,
      products: [],
      analytics: null,
      pageInfo: {
        hasNextPage: false,
        startCursor: null,
        endCursor: null,
      },
    });
  }

  const productIds = [
    ...new Set(
      itemsOnPage.map((item) => `gid://shopify/Product/${item.productId}`),
    ),
  ];

  const products = await storefront
    .query<ProductCardNodes>(PRODUCT_CARDS_QUERY, {
      variables: {
        productIds,
        country: storefront.i18n.country,
        language: storefront.i18n.language,
      },
      storefrontApiVersion: "2023-01",
    })
    .then((result) =>
      result.nodes
        .filter((product) => !!product)
        .map((product) => ({
          ...product,
          wishlistState: getProductState(wishlist, {
            productId: product.id,
          }),
        })),
    );

  const firstItemOnPage = itemsOnPage[0];
  const lastItemOnPage = itemsOnPage[itemsOnPage.length - 1];

  return json({
    wishlist,
    products,
    pageInfo: {
      hasNextPage:
        wishlist.items[wishlist.items.length - 1].id !== lastItemOnPage.id,
      startCursor: firstItemOnPage.id,
      endCursor: lastItemOnPage.id,
    },
  });
}

export async function action(args: ActionArgs) {
  const {
    request,
    context: { wishlistKing },
  } = args;

  switch (request.method) {
    case "POST":
      return await wishlistKing.actions.addWishlistItem(args);
    case "PUT":
      return await wishlistKing.actions.updateWishlistItem(args);
    case "DELETE":
      return await wishlistKing.actions.removeWishlistItem(args);
  }

  throw json(
    { message: "Wishlist action method does not exist" },
    { status: 405, statusText: "Method Not Allowed" },
  );
}

const WishlistPage = () => {
  const loaderData = useLoaderData<typeof loader>();
  const wishlist = useWishlistContext();
  const {
    products: moreProducts,
    pageInfo,
    loadMore,
    loading,
  } = useLoadMore<(typeof loaderData)["products"][0]>({
    productsPerPage: PRODUCTS_PER_PAGE,
    initPage: loaderData.pageInfo,
    wishlist,
  });
  const products = useMemo(
    () =>
      [...loaderData.products, ...moreProducts.slice(PRODUCTS_PER_PAGE)]
        .filter((product) => !!product)
        .filter(
          (product) =>
            product.wishlistState.inWishlist &&
            !wishlist.deletedItems.includes(
              product.wishlistState.wishlistItemId,
            ),
        ),
    [loaderData, moreProducts, wishlist],
  );
  return (
    <WishlistGrid
      products={products}
      hasNextPage={pageInfo.hasNextPage}
      onLoadMore={loadMore}
      loading={loading}
    />
  );
};

export default WishlistPage;

Render Wishlist Components

Render WishlistLink in header.

<WishlistLink />

Render WishlistButton on product page.

<WishlistButton productId={product.id} variantId={selectedVariant?.id} />

FAQs

Package last updated on 31 Oct 2023

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts