@@ -43,9 +51,7 @@ export default async function Navbar() {
) : null}
}>
diff --git a/app/@navbar/search.tsx b/app/@navbar/search.tsx
new file mode 100644
index 0000000000..8a8a33c8c5
--- /dev/null
+++ b/app/@navbar/search.tsx
@@ -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 (
+
+ );
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index 1e17f31d3b..93e81fd7c8 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -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';
@@ -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 (
-
-
-
{children}
-
+
+
+ {navbar}
+ {children}
+
+
);
}
diff --git a/app/page.tsx b/app/page.tsx
index 0fad0ac289..7d407ede83 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -9,7 +9,7 @@ export const metadata = {
}
};
-export default async function HomePage() {
+export default function HomePage() {
return (
<>
diff --git a/app/search/loading.tsx b/app/search/loading.tsx
index 855c371bc2..7b75dd9224 100644
--- a/app/search/loading.tsx
+++ b/app/search/loading.tsx
@@ -7,7 +7,7 @@ export default function Loading() {
.fill(0)
.map((_, index) => {
return (
-
+
);
})}
diff --git a/components/layout/navbar/search.tsx b/components/layout/navbar/search.tsx
deleted file mode 100644
index 551d781c2b..0000000000
--- a/components/layout/navbar/search.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-'use client';
-
-import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
-import { createUrl } from 'lib/utils';
-import { useRouter, useSearchParams } from 'next/navigation';
-
-export default function Search() {
- const router = useRouter();
- const searchParams = useSearchParams();
-
- function onSubmit(e: React.FormEvent
) {
- e.preventDefault();
-
- const val = e.target as HTMLFormElement;
- const search = val.search as HTMLInputElement;
- const newParams = new URLSearchParams(searchParams.toString());
-
- if (search.value) {
- newParams.set('q', search.value);
- } else {
- newParams.delete('q');
- }
-
- router.push(createUrl('/search', newParams));
- }
-
- return (
-
- );
-}
-
-export function SearchSkeleton() {
- return (
-
- );
-}
diff --git a/components/layout/product-grid-items.tsx b/components/layout/product-grid-items.tsx
index ea8a5ebf75..22ed9a5a75 100644
--- a/components/layout/product-grid-items.tsx
+++ b/components/layout/product-grid-items.tsx
@@ -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 (
+
+ );
+ })}
+ >
+ );
+}
+
export default function ProductGridItems({ products }: { products: Product[] }) {
+ const { isPending } = useGlobalTransition();
+
+ if (isPending) {
+ return ;
+ }
+
return (
<>
{products.map((product) => (
diff --git a/components/product/gallery.tsx b/components/product/gallery.tsx
index 0b03557a55..34d90047e9 100644
--- a/components/product/gallery.tsx
+++ b/components/product/gallery.tsx
@@ -4,38 +4,37 @@ 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 (
<>
- {images[imageIndex] && (
+ {images[optimisticIndex] && (
)}
@@ -43,23 +42,25 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
{images.length > 1 ? (
-
{
+ updateIndex(optimisticIndex - 1);
+ }}
className={buttonClassName}
- scroll={false}
>
-
+
-
{
+ updateIndex(optimisticIndex + 1);
+ }}
className={buttonClassName}
- scroll={false}
>
-
+
) : null}
@@ -68,18 +69,16 @@ 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 (
-
- {
+ updateIndex(index);
+ }}
>
-
+
);
})}
diff --git a/components/product/variant-selector.tsx b/components/product/variant-selector.tsx
index 9d47eb5c8a..bb0376020e 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,13 @@ export function VariantSelector({
const router = useRouter();
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 =
!options.length || (options.length === 1 && options[0]?.values.length === 1);
@@ -28,7 +36,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").
@@ -45,14 +53,6 @@ export function VariantSelector({
{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
@@ -62,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)
)
@@ -74,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 (