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

View File

@ -23,6 +23,10 @@ export function VariantSelector({
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [optimisticVariants, setOptimsticVariants] = useOptimistic(variants); 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 [pending, startTransition] = useTransition();
const hasNoOptionsOrJustOneOption = const hasNoOptionsOrJustOneOption =
@ -44,20 +48,11 @@ export function VariantSelector({
return options.map((option) => ( return options.map((option) => (
<dl className="mb-8" key={option.id}> <dl className="mb-8" key={option.id}>
{pending && <div className="">Loading...</div>}
<dt className="mb-4 text-sm uppercase tracking-wide">{option.name}</dt> <dt className="mb-4 text-sm uppercase tracking-wide">{option.name}</dt>
<dd className="flex flex-wrap gap-3"> <dd className="flex flex-wrap gap-3">
{option.values.map((value) => { {option.values.map((value) => {
const optionNameLowerCase = option.name.toLowerCase(); 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: // In order to determine if an option is available for sale, we need to:
// //
// 1. Filter out all other param state // 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 // 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, // 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. // 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( options.find(
(option) => option.name.toLowerCase() === key && option.values.includes(value) (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. // The option is active if it's in the url params.
const isActive = searchParams.get(optionNameLowerCase) === value; const isActive = optimisticOptions.get(optionNameLowerCase) === value;
return ( return (
<button <button
@ -89,10 +84,9 @@ export function VariantSelector({
onClick={() => { onClick={() => {
startTransition(() => { startTransition(() => {
const newOptimisticVariants = optimisticVariants.map((variant) => { 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) => { const updatedOptions = variant.selectedOptions.map((option) => {
if (option.name.toLowerCase() === optionNameLowerCase) { if (option.name.toLowerCase() === optionNameLowerCase) {
return { ...option, value: value, isActive: true }; // Set active optimistically return { ...option, value: value };
} }
return option; return option;
}); });
@ -100,7 +94,12 @@ export function VariantSelector({
return { ...variant, selectedOptions: updatedOptions }; 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 // Navigate without page reload
router.replace(optionUrl, { scroll: false }); router.replace(optionUrl, { scroll: false });