From dffafc2a4529a1c189a21997358fce6d8a1c8b45 Mon Sep 17 00:00:00 2001 From: Lee Robinson Date: Thu, 18 Apr 2024 19:02:43 -0700 Subject: [PATCH 1/4] test --- components/product/variant-selector.tsx | 26 +++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/components/product/variant-selector.tsx b/components/product/variant-selector.tsx index 9d47eb5c8a..3940472e2f 100644 --- a/components/product/variant-selector.tsx +++ b/components/product/variant-selector.tsx @@ -4,6 +4,7 @@ import clsx from 'clsx'; import { ProductOption, ProductVariant } from 'lib/shopify/types'; import { createUrl } from 'lib/utils'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { useOptimistic, useTransition } from 'react'; type Combination = { id: string; @@ -21,6 +22,9 @@ export function VariantSelector({ const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); + const [optimisticVariants, setOptimsticVariants] = useOptimistic(variants); + const [pending, startTransition] = useTransition(); + const hasNoOptionsOrJustOneOption = !options.length || (options.length === 1 && options[0]?.values.length === 1); @@ -28,7 +32,7 @@ export function VariantSelector({ return null; } - const combinations: Combination[] = variants.map((variant) => ({ + const combinations: Combination[] = optimisticVariants.map((variant) => ({ id: variant.id, availableForSale: variant.availableForSale, // Adds key / value pairs for each variant (ie. "color": "Black" and "size": 'M"). @@ -40,6 +44,7 @@ export function VariantSelector({ return options.map((option) => (
+ {pending &&
Loading...
}
{option.name}
{option.values.map((value) => { @@ -82,7 +87,24 @@ export function VariantSelector({ aria-disabled={!isAvailableForSale} disabled={!isAvailableForSale} onClick={() => { - router.replace(optionUrl, { scroll: false }); + 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; + }); + + return { ...variant, selectedOptions: updatedOptions }; + }); + + setOptimsticVariants(newOptimisticVariants); // Update the state optimistically + + // Navigate without page reload + router.replace(optionUrl, { scroll: false }); + }); }} title={`${option.name} ${value}${!isAvailableForSale ? ' (Out of Stock)' : ''}`} className={clsx( From bb3bc33e672a9cb0f222dbc1f5f32450d2e50626 Mon Sep 17 00:00:00 2001 From: Lee Robinson Date: Thu, 18 Apr 2024 19:50:09 -0700 Subject: [PATCH 2/4] Moar --- components/product/gallery.tsx | 71 ++++++++++++++----------- components/product/variant-selector.tsx | 27 +++++----- 2 files changed, 52 insertions(+), 46 deletions(-) diff --git a/components/product/gallery.tsx b/components/product/gallery.tsx index 0b03557a55..9290f8ea65 100644 --- a/components/product/gallery.tsx +++ b/components/product/gallery.tsx @@ -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 ( <>
- {images[imageIndex] && ( + {images[optimisticIndex] && ( {images[imageIndex]?.altText )} @@ -43,23 +44,29 @@ export function Gallery({ images }: { images: { src: string; altText: string }[] {images.length > 1 ? (
- { + startTransition(() => { + updateIndex(optimisticIndex - 1); + }); + }} className={buttonClassName} - scroll={false} > - +
- { + startTransition(() => { + updateIndex(optimisticIndex + 1); + }); + }} className={buttonClassName} - scroll={false} > - +
) : null} @@ -68,18 +75,18 @@ export function Gallery({ images }: { images: { src: string; altText: string }[] {images.length > 1 ? (
    {images.map((image, index) => { - const isActive = index === imageIndex; - const imageSearchParams = new URLSearchParams(searchParams.toString()); - - imageSearchParams.set('image', index.toString()); + const isActive = index === optimisticIndex; return (
  • - { + startTransition(() => { + updateIndex(index); + }); + }} > - +
  • ); })} diff --git a/components/product/variant-selector.tsx b/components/product/variant-selector.tsx index 3940472e2f..bb0376020e 100644 --- a/components/product/variant-selector.tsx +++ b/components/product/variant-selector.tsx @@ -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) => (
    - {pending &&
    Loading...
    }
    {option.name}
    {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 ( -
    - }> - - -
    +
    {children}
    {menu.length ? (
      {menu.map((item: Menu) => ( diff --git a/components/layout/navbar/index.tsx b/app/@navbar/page.tsx similarity index 83% rename from components/layout/navbar/index.tsx rename to app/@navbar/page.tsx index f7d2f6af9a..45e04bb018 100644 --- a/components/layout/navbar/index.tsx +++ b/app/@navbar/page.tsx @@ -6,17 +6,25 @@ import { Menu } from 'lib/shopify/types'; import Link from 'next/link'; import { Suspense } from 'react'; import MobileMenu from './mobile-menu'; -import Search, { SearchSkeleton } from './search'; +import Search from './search'; + const { SITE_NAME } = process.env; -export default async function Navbar() { +export default async function Navbar({ + searchParams +}: { + searchParams: { [key: string]: string | string[] | undefined }; +}) { const menu = await getMenu('next-js-frontend-header-menu'); + const search = searchParams.q ? (searchParams.q as string) : ''; return (