mirror of
https://github.com/vercel/commerce.git
synced 2025-05-12 20:57:51 +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 { 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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -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 });
|
||||||
|
Loading…
x
Reference in New Issue
Block a user