Skip to content

Commit

Permalink
Better variant selector (#945)
Browse files Browse the repository at this point in the history
  • Loading branch information
typeofweb authored Oct 13, 2023
1 parent 10332b7 commit 98d00b4
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 63 deletions.
4 changes: 3 additions & 1 deletion src/app/(main)/products/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ export default async function Page(props: { params: { slug: string }; searchPara
: ""}
</p>

{variants && <VariantSelector variants={variants} />}
{variants && (
<VariantSelector selectedVariant={selectedVariant} variants={variants} product={product} />
)}
{description && (
<div className="mt-8 space-y-6">
<div dangerouslySetInnerHTML={{ __html: description }}></div>
Expand Down
12 changes: 1 addition & 11 deletions src/graphql/ProductDetails.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,7 @@ query ProductDetails($slug: String!) {
name
}
variants {
id
name
quantityAvailable
pricing {
price {
gross {
currency
amount
}
}
}
...VariantDetails
}
}
}
13 changes: 13 additions & 0 deletions src/graphql/VariantDetailsFragment.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
fragment VariantDetails on ProductVariant {
id
name
quantityAvailable
pricing {
price {
gross {
currency
amount
}
}
}
}
82 changes: 31 additions & 51 deletions src/ui/components/VariantSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,76 +1,56 @@
"use client";

import { clsx } from "clsx";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect } from "react";

export const createPathname = (pathname: string, params: URLSearchParams) => {
const paramsString = params.toString();
const queryString = `${paramsString.length ? "?" : ""}${paramsString}`;

return `${pathname}${queryString}`;
};

export function VariantSelector(props: {
variants: { id: string; name: string; quantityAvailable?: number | null }[];
import Link from "next/link";
import { redirect } from "next/navigation";
import { type ProductListItemFragment, type VariantDetailsFragment } from "@/gql/graphql";

export function VariantSelector({
variants,
product,
selectedVariant,
}: {
variants: readonly VariantDetailsFragment[];
product: ProductListItemFragment;
selectedVariant?: VariantDetailsFragment;
}) {
const { variants } = props;

const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();

const selectVariant = useCallback(
(variantID: string, replace = false) => {
const params = new URLSearchParams(searchParams);
params.set("variant", variantID);

const variantPathname = createPathname(pathname, params);
if (replace) {
router.replace(variantPathname, { scroll: false });
} else {
router.push(variantPathname, { scroll: false });
}
},
[pathname, router, searchParams],
);

useEffect(() => {
if (variants.length === 1 && variants[0].quantityAvailable) {
selectVariant(variants[0].id, true);
}
}, [selectVariant, variants]);
if (!selectedVariant && variants.length === 1 && variants[0]?.quantityAvailable) {
redirect(getHrefForVariant(product, variants[0]));
}

return (
<fieldset className="my-4" role="radiogroup" data-testid="VariantSelector">
<legend className="sr-only">Variants</legend>
<div className="flex flex-wrap gap-3">
{variants.length > 1 &&
variants.map((variant) => {
const isDisabled = !variant.quantityAvailable;
const isCurrentVariant = selectedVariant?.id === variant.id;
return (
<button
<Link
key={variant.id}
type="button"
onClick={() => {
if (variant.quantityAvailable) {
selectVariant(variant.id);
}
}}
href={isDisabled ? "#" : getHrefForVariant(product, variant)}
className={clsx(
searchParams.get("variant") === variant.id
isCurrentVariant
? "border-transparent bg-neutral-900 text-white hover:bg-neutral-800"
: "border-neutral-200 bg-white text-neutral-900 hover:bg-neutral-100",
"relative flex min-w-[8ch] items-center justify-center overflow-hidden text-ellipsis whitespace-nowrap rounded border p-3 text-center text-sm font-semibold focus-within:outline focus-within:outline-2 aria-disabled:cursor-not-allowed aria-disabled:bg-neutral-100 aria-disabled:opacity-50",
isDisabled && "pointer-events-none",
)}
role="radio"
aria-checked={searchParams.get("variant") === variant.id}
aria-disabled={!variant.quantityAvailable}
tabIndex={isDisabled ? -1 : undefined}
aria-checked={isCurrentVariant}
aria-disabled={isDisabled}
>
{variant.name}
</button>
</Link>
);
})}
</div>
</fieldset>
);
}

function getHrefForVariant(product: ProductListItemFragment, variant: VariantDetailsFragment): string {
const pathname = `/products/${encodeURIComponent(product.slug)}`;
const query = new URLSearchParams({ variant: variant.id });
return `${pathname}?${query.toString()}`;
}

0 comments on commit 98d00b4

Please sign in to comment.