mirror of
https://github.com/vercel/commerce.git
synced 2025-05-12 12:47:50 +00:00
Moar
This commit is contained in:
parent
dffafc2a45
commit
bb3bc33e67
@ -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>
|
||||
);
|
||||
})}
|
||||
|
@ -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 });
|
||||
|
Loading…
x
Reference in New Issue
Block a user