This commit is contained in:
Lee Robinson 2024-04-18 19:50:09 -07:00
parent dffafc2a45
commit bb3bc33e67
2 changed files with 52 additions and 46 deletions

View File

@ -4,38 +4,39 @@ import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
import { GridTileImage } from 'components/grid/tile';
import { createUrl } from 'lib/utils';
import Image from 'next/image';
import Link from 'next/link';
import { usePathname, useSearchParams } from 'next/navigation';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useOptimistic, useTransition } from 'react';
export function Gallery({ images }: { images: { src: string; altText: string }[] }) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const imageSearchParam = searchParams.get('image');
const imageIndex = imageSearchParam ? parseInt(imageSearchParam) : 0;
const nextSearchParams = new URLSearchParams(searchParams.toString());
const nextImageIndex = imageIndex + 1 < images.length ? imageIndex + 1 : 0;
nextSearchParams.set('image', nextImageIndex.toString());
const nextUrl = createUrl(pathname, nextSearchParams);
const previousSearchParams = new URLSearchParams(searchParams.toString());
const previousImageIndex = imageIndex === 0 ? images.length - 1 : imageIndex - 1;
previousSearchParams.set('image', previousImageIndex.toString());
const previousUrl = createUrl(pathname, previousSearchParams);
const [optimisticIndex, setOptimisticIndex] = useOptimistic(imageIndex);
// eslint-disable-next-line no-unused-vars
const [pending, startTransition] = useTransition();
const buttonClassName =
'h-full px-6 transition-all ease-in-out hover:scale-110 hover:text-black dark:hover:text-white flex items-center justify-center';
function updateIndex(newIndex: number) {
setOptimisticIndex(newIndex);
const newSearchParams = new URLSearchParams(searchParams.toString());
newSearchParams.set('image', newIndex.toString());
router.replace(createUrl(pathname, newSearchParams), { scroll: false });
}
return (
<>
<div className="relative aspect-square h-full max-h-[550px] w-full overflow-hidden">
{images[imageIndex] && (
{images[optimisticIndex] && (
<Image
className="h-full w-full object-contain"
fill
sizes="(min-width: 1024px) 66vw, 100vw"
alt={images[imageIndex]?.altText as string}
src={images[imageIndex]?.src as string}
alt={images[optimisticIndex]?.altText as string}
src={images[optimisticIndex]?.src as string}
priority={true}
/>
)}
@ -43,23 +44,29 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
{images.length > 1 ? (
<div className="absolute bottom-[15%] flex w-full justify-center">
<div className="mx-auto flex h-11 items-center rounded-full border border-white bg-neutral-50/80 text-neutral-500 backdrop-blur dark:border-black dark:bg-neutral-900/80">
<Link
<button
aria-label="Previous product image"
href={previousUrl}
onClick={() => {
startTransition(() => {
updateIndex(optimisticIndex - 1);
});
}}
className={buttonClassName}
scroll={false}
>
<ArrowLeftIcon className="h-5" />
</Link>
</button>
<div className="mx-1 h-6 w-px bg-neutral-500"></div>
<Link
<button
aria-label="Next product image"
href={nextUrl}
onClick={() => {
startTransition(() => {
updateIndex(optimisticIndex + 1);
});
}}
className={buttonClassName}
scroll={false}
>
<ArrowRightIcon className="h-5" />
</Link>
</button>
</div>
</div>
) : null}
@ -68,18 +75,18 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
{images.length > 1 ? (
<ul className="my-12 flex items-center justify-center gap-2 overflow-auto py-1 lg:mb-0">
{images.map((image, index) => {
const isActive = index === imageIndex;
const imageSearchParams = new URLSearchParams(searchParams.toString());
imageSearchParams.set('image', index.toString());
const isActive = index === optimisticIndex;
return (
<li key={image.src} className="h-20 w-20">
<Link
aria-label="Enlarge product image"
href={createUrl(pathname, imageSearchParams)}
scroll={false}
<button
aria-label="Select product image"
className="h-full w-full"
onClick={() => {
startTransition(() => {
updateIndex(index);
});
}}
>
<GridTileImage
alt={image.altText}
@ -88,7 +95,7 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
height={80}
active={isActive}
/>
</Link>
</button>
</li>
);
})}

View File

@ -23,6 +23,10 @@ export function VariantSelector({
const pathname = usePathname();
const searchParams = useSearchParams();
const [optimisticVariants, setOptimsticVariants] = useOptimistic(variants);
const [optimisticOptions, setOptimisticOptions] = useOptimistic(
new URLSearchParams(searchParams.toString())
);
// eslint-disable-next-line no-unused-vars
const [pending, startTransition] = useTransition();
const hasNoOptionsOrJustOneOption =
@ -44,20 +48,11 @@ export function VariantSelector({
return options.map((option) => (
<dl className="mb-8" key={option.id}>
{pending && <div className="">Loading...</div>}
<dt className="mb-4 text-sm uppercase tracking-wide">{option.name}</dt>
<dd className="flex flex-wrap gap-3">
{option.values.map((value) => {
const optionNameLowerCase = option.name.toLowerCase();
// Base option params on current params so we can preserve any other param state in the url.
const optionSearchParams = new URLSearchParams(searchParams.toString());
// Update the option params using the current option to reflect how the url *would* change,
// if the option was clicked.
optionSearchParams.set(optionNameLowerCase, value);
const optionUrl = createUrl(pathname, optionSearchParams);
// In order to determine if an option is available for sale, we need to:
//
// 1. Filter out all other param state
@ -67,7 +62,7 @@ export function VariantSelector({
// This is the "magic" that will cross check possible variant combinations and preemptively
// disable combinations that are not available. For example, if the color gray is only available in size medium,
// then all other sizes should be disabled.
const filtered = Array.from(optionSearchParams.entries()).filter(([key, value]) =>
const filtered = Array.from(optimisticOptions.entries()).filter(([key, value]) =>
options.find(
(option) => option.name.toLowerCase() === key && option.values.includes(value)
)
@ -79,7 +74,7 @@ export function VariantSelector({
);
// The option is active if it's in the url params.
const isActive = searchParams.get(optionNameLowerCase) === value;
const isActive = optimisticOptions.get(optionNameLowerCase) === value;
return (
<button
@ -89,10 +84,9 @@ export function VariantSelector({
onClick={() => {
startTransition(() => {
const newOptimisticVariants = optimisticVariants.map((variant) => {
// Assume every variant has an 'options' array where each option has an 'isActive' property.
const updatedOptions = variant.selectedOptions.map((option) => {
if (option.name.toLowerCase() === optionNameLowerCase) {
return { ...option, value: value, isActive: true }; // Set active optimistically
return { ...option, value: value };
}
return option;
});
@ -100,7 +94,12 @@ export function VariantSelector({
return { ...variant, selectedOptions: updatedOptions };
});
setOptimsticVariants(newOptimisticVariants); // Update the state optimistically
optimisticOptions.set(optionNameLowerCase, value);
setOptimsticVariants(newOptimisticVariants);
setOptimisticOptions(new URLSearchParams(optimisticOptions.toString()));
const optionUrl = createUrl(pathname, optimisticOptions);
// Navigate without page reload
router.replace(optionUrl, { scroll: false });