Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make variant / image updates use useOptimistic and startTransition #1327

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/@navbar/default.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Navbar from 'app/@navbar/page';

export default Navbar;
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
import { Dialog, Transition } from '@headlessui/react';
import Link from 'next/link';
import { usePathname, useSearchParams } from 'next/navigation';
import { Fragment, Suspense, useEffect, useState } from 'react';
import { Fragment, useEffect, useState } from 'react';

import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline';
import { Menu } from 'lib/shopify/types';
import Search, { SearchSkeleton } from './search';

export default function MobileMenu({ menu }: { menu: Menu[] }) {
export default function MobileMenu({
menu,
children
}: {
menu: Menu[];
children: React.ReactNode;
}) {
const pathname = usePathname();
const searchParams = useSearchParams();
const [isOpen, setIsOpen] = useState(false);
Expand Down Expand Up @@ -71,11 +76,7 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
<XMarkIcon className="h-6" />
</button>

<div className="mb-4 w-full">
<Suspense fallback={<SearchSkeleton />}>
<Search />
</Suspense>
</div>
<div className="mb-4 w-full">{children}</div>
{menu.length ? (
<ul className="flex w-full flex-col">
{menu.map((item: Menu) => (
Expand Down
18 changes: 12 additions & 6 deletions components/layout/navbar/index.tsx → app/@navbar/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<nav className="relative flex items-center justify-between p-4 lg:px-6">
<div className="block flex-none md:hidden">
<Suspense fallback={null}>
<MobileMenu menu={menu} />
<MobileMenu menu={menu}>
<Search value={search} />
</MobileMenu>
</Suspense>
</div>
<div className="flex w-full items-center">
Expand All @@ -43,9 +51,7 @@ export default async function Navbar() {
) : null}
</div>
<div className="hidden justify-center md:flex md:w-1/3">
<Suspense fallback={<SearchSkeleton />}>
<Search />
</Suspense>
<Search value={search} />
</div>
<div className="flex justify-end md:w-1/3">
<Suspense fallback={<OpenCart />}>
Expand Down
34 changes: 34 additions & 0 deletions app/@navbar/search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';

import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { useGlobalTransition } from 'lib/transition';
import { useRouter } from 'next/navigation';

export default function Search({ value }: { value: string }) {
const router = useRouter();
const { startTransition } = useGlobalTransition();

function searchAction(formData: FormData) {
const search = formData.get('search') as string;
startTransition(() => {
router.push(`/search?q=${search}`);
});
}

return (
<form action={searchAction} className="w-max-[550px] relative w-full lg:w-80 xl:w-full">
<input
key={value}
type="text"
name="search"
placeholder="Search for products..."
autoComplete="off"
defaultValue={value}
className="w-full rounded-lg border bg-white px-4 py-2 text-sm text-black placeholder:text-neutral-500 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400"
/>
<div className="absolute right-0 top-0 mr-3 flex h-full items-center">
<MagnifyingGlassIcon className="h-4" />
</div>
</form>
);
}
20 changes: 14 additions & 6 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Navbar from 'components/layout/navbar';
import { GeistSans } from 'geist/font/sans';
import { GlobalTransitionProvider } from 'lib/transition';
import { ensureStartsWith } from 'lib/utils';
import { ReactNode } from 'react';
import './globals.css';
Expand Down Expand Up @@ -31,13 +31,21 @@ export const metadata = {
})
};

export default async function RootLayout({ children }: { children: ReactNode }) {
export default async function RootLayout({
children,
navbar
}: {
children: ReactNode;
navbar: ReactNode;
}) {
return (
<html lang="en" className={GeistSans.variable}>
<body className="bg-neutral-50 text-black selection:bg-teal-300 dark:bg-neutral-900 dark:text-white dark:selection:bg-pink-500 dark:selection:text-white">
<Navbar />
<main>{children}</main>
</body>
<GlobalTransitionProvider>
<body className="bg-neutral-50 text-black selection:bg-teal-300 dark:bg-neutral-900 dark:text-white dark:selection:bg-pink-500 dark:selection:text-white">
{navbar}
<main>{children}</main>
</body>
</GlobalTransitionProvider>
</html>
);
}
2 changes: 1 addition & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const metadata = {
}
};

export default async function HomePage() {
export default function HomePage() {
return (
<>
<ThreeItemGrid />
Expand Down
2 changes: 1 addition & 1 deletion app/search/loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function Loading() {
.fill(0)
.map((_, index) => {
return (
<Grid.Item key={index} className="animate-pulse bg-neutral-100 dark:bg-neutral-900" />
<Grid.Item key={index} className="animate-pulse bg-neutral-100 dark:bg-neutral-800" />
);
})}
</Grid>
Expand Down
57 changes: 0 additions & 57 deletions components/layout/navbar/search.tsx

This file was deleted.

26 changes: 26 additions & 0 deletions components/layout/product-grid-items.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,35 @@
'use client';

import Grid from 'components/grid';
import { GridTileImage } from 'components/grid/tile';
import { Product } from 'lib/shopify/types';
import { useGlobalTransition } from 'lib/transition';
import Link from 'next/link';

function Loading() {
return (
<>
{Array(12)
.fill(0)
.map((_, index) => {
return (
<Grid.Item
key={index}
className="h-full w-full animate-pulse bg-neutral-100 dark:bg-neutral-800"
/>
);
})}
</>
);
}

export default function ProductGridItems({ products }: { products: Product[] }) {
const { isPending } = useGlobalTransition();

if (isPending) {
return <Loading />;
}

return (
<>
{products.map((product) => (
Expand Down
63 changes: 31 additions & 32 deletions components/product/gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,62 +4,63 @@ 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 } 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);

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}
/>
)}

{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}
formAction={() => {
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}
formAction={() => {
updateIndex(optimisticIndex + 1);
}}
className={buttonClassName}
scroll={false}
>
<ArrowRightIcon className="h-5" />
</Link>
</button>
</div>
</div>
) : null}
Expand All @@ -68,18 +69,16 @@ 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"
formAction={() => {
updateIndex(index);
}}
>
<GridTileImage
alt={image.altText}
Expand All @@ -88,7 +87,7 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
height={80}
active={isActive}
/>
</Link>
</button>
</li>
);
})}
Expand Down
Loading
Loading