I'm using nextjs app router version 15.
optimize the following ProductDetailModel as much as possible, improve performance, improve stability,  improve animation and improve compatbility especially for Safari browser:
Keep the design as it is now however you can rearrange the code so it look exactly like how it look now... if you want...
'use client'
import React, { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Minus, Plus, ShoppingCart } from 'lucide-react';
import { useNotification } from "@/components/NotificationContext";
import '@/components/NotificationStyles.css';
import Link from "next/link";

const formatPrice = (price) => {
    const formattedPrice = price >= 10000 ? price.toLocaleString('en-US') : price.toString();
    return `${formattedPrice} IQD`;
};

const ProductModal = ({ isOpen, onClose, productData }) => {
    const [selectedSize, setSelectedSize] = useState(null);
    const [quantity, setQuantity] = useState(1);
    const [isInCart, setIsInCart] = useState(false);
    const [totalPrice, setTotalPrice] = useState(0);
    const { addNotification } = useNotification();

    const product = productData.product;
    const updateTotalPrice = useCallback(() => {
        if (selectedSize) {
            setTotalPrice(parseFloat(selectedSize.price) * quantity);
        }
    }, [selectedSize, quantity]);
    const checkIfInCart = useCallback(() => {
        const cart = JSON.parse(localStorage.getItem('cart')) || [];
        const isItemInCart = cart.some(item =>
            item.product_id === product.id &&
            item.size_id === selectedSize?.id
        );
        setIsInCart(isItemInCart);
    }, [product.id, selectedSize]);
    useEffect(() => {
        if (product && product.sizes && product.sizes.length > 0) {
            setSelectedSize(product.sizes[0]);
        }
    }, [product]);

    useEffect(() => {
        updateTotalPrice();
    }, [selectedSize, quantity, updateTotalPrice]);

    useEffect(() => {
        checkIfInCart();
    }, [selectedSize, checkIfInCart]);


    const handleAddToCart = useCallback(() => {
        if (selectedSize) {
            const cartItem = {
                product_id: product.id,
                size_id: selectedSize.id,
                qty: quantity,
                price: selectedSize.price
            };

            const cart = JSON.parse(localStorage.getItem('cart')) || [];
            const existingItemIndex = cart.findIndex(item =>
                item.product_id === cartItem.product_id &&
                item.size_id === cartItem.size_id
            );

            if (existingItemIndex > -1) {
                cart[existingItemIndex].qty += quantity;
            } else {
                cart.push(cartItem);
            }

            localStorage.setItem('cart', JSON.stringify(cart));
            setIsInCart(true);
            addNotification('success', 'Added to cart successfully');
            checkIfInCart();
        }
    }, [product.id, selectedSize, quantity, addNotification, checkIfInCart]);

    if (!isOpen || !product) return null;

    return (
        <AnimatePresence>
            <motion.div
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                exit={{ opacity: 0 }}
                className="fixed font-serif inset-0 bg-black bg-opacity-30 flex items-end justify-center z-50"
                onClick={onClose}
            >
                <motion.div
                    initial={{ y: "100%" }}
                    animate={{ y: 0 }}
                    exit={{ y: "100%" }}
                    transition={{ type: "spring", damping: 25, stiffness: 500 }}
                    className="bg-white rounded-t-3xl w-full p-6 relative"
                    onClick={(e) => e.stopPropagation()}
                >
                    <div className="w-9 h-1 bg-[#00003C] opacity-70 rounded-full mx-auto mb-6" />

                    <h2 className="text-xl mb-4 font-sans">{product.name}</h2>

                    {product.has_size && (
                        <div className="mb-4">
                            <select
                                value={selectedSize ? selectedSize.id : ''}
                                onChange={(e) => setSelectedSize(product.sizes.find(size => size.id.toString() === e.target.value))}
                                className="w-44 p-2 border border-[rgba(105,92,92,0.5)] rounded-lg font-serif text-lg"
                            >
                                <option value="" disabled={true}>Select a Size</option>
                                {product.sizes.map((size) => (
                                    <option key={size.id} value={size.id}>
                                        {size.name}
                                    </option>
                                ))}
                            </select>
                            {!selectedSize && (
                                <p className="text-[#C91C1C] text-xs mt-2 font-sans">Please choose Size</p>
                            )}
                        </div>
                    )}

                    {product.has_color && product.colors && product.colors.length > 0 && (
                        <div className="mb-6">
                            <h3 className="text-xl font-medium mb-2 font-sans">Color</h3>
                            <div className="flex space-x-5 mb-2">
                                {product.colors.map((color) => (
                                    <button
                                        key={color.id}
                                        className={`w-20 h-20 rounded-full border-2 ${
                                            selectedSize && selectedSize.color_id === color.id ? 'border-[rgba(105,92,92,0.5)]' : 'border-transparent'
                                        } flex items-center justify-center`}
                                    >
                                        <div
                                            className="w-15 h-15 rounded-full"
                                            style={{ backgroundColor: color.hex }}
                                        />
                                    </button>
                                ))}
                            </div>
                            <div className="flex justify-between px-2">
                                {product.colors.map((color) => (
                                    <span key={color.id} className="text-sm font-serif">{color.name}</span>
                                ))}
                            </div>
                        </div>
                    )}

                    <div className="flex justify-between items-center mb-6">
                        <p className="text-lg font-medium">
                            {formatPrice(totalPrice)}
                        </p>
                        <div className="flex items-center space-x-4">
                            <button
                                className="w-8 h-8 rounded-full border border-gray-300 flex items-center justify-center"
                                onClick={() => setQuantity(Math.max(1, quantity - 1))}
                            >
                                <Minus className="w-4 h-4 text-[#3B5345]" />
                            </button>
                            <span className="text-lg">{quantity}</span>
                            <button
                                className="w-8 h-8 rounded-full border border-gray-300 flex items-center justify-center"
                                onClick={() => setQuantity(quantity + 1)}
                            >
                                <Plus className="w-4 h-4 text-[#3B5345]" />
                            </button>
                        </div>
                    </div>

                    <div className="flex space-x-4">
                        {isInCart ? (
                            <Link
                                href="/cart"
                                prefetch={false}
                                className="flex-1 font-serif bg-[rgba(59,83,69,0.05)] text-[#3B5345] py-3 rounded-lg font-medium text-base md:text-lg flex items-center justify-center transition duration-300 border border-[#3B5345]"
                            >
                                <ShoppingCart className="mr-2" />
                                Buy Now
                            </Link>
                        ) : (
                            <button
                                className={`flex-1 ${selectedSize ? 'bg-[#3B5345] text-white' : 'bg-gray-300 text-gray-500 cursor-not-allowed'} py-3 rounded-lg font-medium text-base md:text-lg flex items-center justify-center`}
                                onClick={handleAddToCart}
                                disabled={!selectedSize}
                            >
                                <ShoppingCart className="mr-2" />
                                Add To Cart
                            </button>
                        )}
                        <button
                            className="flex-1 bg-gray-200 text-gray-800 py-3 rounded-lg font-medium text-base md:text-lg"
                            onClick={onClose}
                        >
                            Close
                        </button>
                    </div>
                </motion.div>
            </motion.div>
        </AnimatePresence>
    );
};

export default React.memo(ProductModal);
Nextjs 15 RC docs:
Next.js 15 RC
Delba de Oliveira@delba_oliveiraZack Tanner@zt1072
14–17 minutes

The Next.js 15 Release Candidate (RC) is now available. This early version allows you to test the latest features before the upcoming stable release.

    React: Support for the React 19 RC, React Compiler (Experimental), and hydration error improvements
    Caching: fetch requests, GET Route Handlers, and client navigations are no longer cached by default
    Partial Prerendering (Experimental): New Layout and Page config option for incremental adoption
    next/after (Experimental): New API to execute code after a response has finished streaming
    create-next-app: Updated design and a new flag to enable Turbopack in local development
    Bundling external packages (Stable): New config options for App and Pages Router

Try the Next.js 15 RC today:

npm install next@rc react@rc react-dom@rc

React 19 RC

The Next.js App Router is built on the React canary channel

for frameworks, which has allowed developers to use and provide feedback on these new React APIs before the v19 release.

Next.js 15 RC now supports React 19 RC, which includes new features for both the client and server like Actions.

Read the Next.js 15 upgrade guide
, the React 19 upgrade guide, and watch the React Conf Keynote

to learn more.

    Note: Some third party libraries may not be compatible with React 19 yet.

React Compiler (Experimental)

The React Compiler
is a new experimental compiler created by the React team at Meta. The compiler understands your code at a deep level through its understanding of plain JavaScript semantics and the Rules of React

, which allows it to add automatic optimizations to your code. The compiler reduces the amount of manual memoization developers have to do through APIs such as useMemo and useCallback - making code simpler, easier to maintain, and less error prone.

With Next.js 15, we've added support for the React Compiler

.

Install babel-plugin-react-compiler:

npm install babel-plugin-react-compiler

Then, add experimental.reactCompiler option in next.config.js:

const nextConfig = {
  experimental: {
    reactCompiler: true,
  },
};
 
module.exports = nextConfig;

Optionally, you can configure the compiler to run in "opt-in" mode as follows:

const nextConfig = {
  experimental: {
    reactCompiler: {
      compilationMode: 'annotation',
    },
  },
};
 
module.exports = nextConfig;

    Note: The React Compiler is currently only possible to use in Next.js through a Babel plugin, which could result in slower build times.

Learn more about the React Compiler
, and the available Next.js config options

.
Hydration error improvements

Next.js 14.1 made improvements to error messages and hydration errors. Next.js 15 continues to build on those by adding an improved hydration error view. Hydration errors now display the source code of the error with suggestions on how to address the issue.

For example, this was a previous hydration error message in Next.js 14.1:
Hydration error message in Next.js 14.1

Next.js 15 RC has improved this to:
Hydration error message improved in Next.js 15 RC
Caching updates

Next.js App Router launched with opinionated caching defaults. These were designed to provide the most performant option by default with the ability to opt out when required.

Based on your feedback, we re-evaluated our caching heuristics

and how they would interact with projects like Partial Prerendering (PPR) and with third party libraries using fetch.

With Next.js 15, we’re changing the caching default for fetch requests, GET Route Handlers, and Client Router Cache from cached by default to uncached by default. If you want to retain the previous behavior, you can continue to opt-into caching.

We're continuing to improve caching in Next.js in the coming months and we'll share more details in the Next.js 15 GA announcement.
fetch Requests are no longer cached by default

Next.js uses the Web fetch API

 cache option to configure how a server-side fetch request interacts with the framework's persistent HTTP cache:

fetch('https://...', { cache: 'force-cache' | 'no-store' });

    no-store - fetch a resource from a remote server on every request and do not update the cache
    force-cache - fetch a resource from the cache (if it exists) or a remote server and update the cache

In Next.js 14, force-cache was used by default if a cache option was not provided, unless a dynamic function or dynamic config option was used.

In Next.js 15, no-store is used by default if a cache option is not provided. This means fetch requests will not be cached by default.

You can still opt into caching fetch requests by:

    Setting the cache option

to force-cache in a single fetch call
Setting the dynamic route config option
to 'force-static' for a single route
Setting the fetchCache route config option

    to 'default-cache' to override all fetch requests in a Layout or Page to use force-cache unless they explicitly specify their own cache option

GET Route Handlers are no longer cached by default

In Next 14, Route Handlers that used the GET HTTP method were cached by default unless they used a dynamic function or dynamic config option. In Next.js 15, GET functions are not cached by default.

You can still opt into caching using a static route config option such as export dynamic = 'force-static'.

Special Route Handlers like sitemap.ts
, opengraph-image.tsx, and icon.tsx, and other metadata files

remain static by default unless they use dynamic functions or dynamic config options.
Client Router Cache no longer caches Page components by default

In Next.js 14.2.0, we introduced an experimental staleTimes
flag to allow custom configuration of the Router Cache

.

In Next.js 15, this flag still remains accessible, but we are changing the default behavior to have a staleTime of 0 for Page segments. This means that as you navigate around your app, the client will always reflect the latest data from the Page component(s) that become active as part of the navigation. However, there are still important behaviors that remain unchanged:

    Shared layout data won't be refetched from the server to continue to support partial rendering

.
Back/forward navigation will still restore from cache to ensure the browser can restore scroll position.
Loading.js

    will remain cached for 5 minutes (or the value of the staleTimes.static configuration).

You can opt into the previous Client Router Cache behavior by setting the following configuration:

const nextConfig = {
  experimental: {
    staleTimes: {
      dynamic: 30,
    },
  },
};
 
module.exports = nextConfig;

Incremental adoption of Partial Prerendering (Experimental)

In Next.js 14, we introduced Partial Prerendering (PPR) - an optimization that combines static and dynamic rendering

on the same page.

Next.js currently defaults to static rendering unless you use dynamic functions

such as cookies(), headers(), and uncached data requests. These APIs opt an entire route into dynamic rendering. With PPR, you can wrap any dynamic UI in a Suspense boundary. When a new request comes in, Next.js will immediately serve a static HTML shell, then render and stream the dynamic parts in the same HTTP request.

To allow for incremental adoption, we’ve added an experimental_ppr route config option for opting specific Layouts and Pages into PPR:

import { Suspense } from "react"
import { StaticComponent, DynamicComponent } from "@/app/ui"
 
export const experimental_ppr = true
 
export default function Page() {
  return {
     <>
	     <StaticComponent />
	     <Suspense fallback={...}>
		     <DynamicComponent />
	     </Suspense>
     </>
  };
}

To use the new option, you’ll need to set the experimental.ppr config in your next.config.js file to 'incremental':

const nextConfig = {
  experimental: {
    ppr: 'incremental',
  },
};
 
module.exports = nextConfig;

Once all the segments have PPR enabled, it’ll be considered safe for you to set the ppr value to true, and enable it for the entire app and all future routes.

We will share more about our PPR roadmap in our Next.js 15 GA blog post.

Learn more about Partial Prerendering

.
Executing code after a response with next/after (Experimental)

When processing a user request, the server typically performs tasks directly related to computing the response. However, you may need to perform tasks such as logging, analytics, and other external system synchronization.

Since these tasks are not directly related to the response, the user should not have to wait for them to complete. Deferring the work after responding to the user poses a challenge because serverless functions stop computation immediately after the response is closed.

after() is a new experimental API that solves this problem by allowing you to schedule work to be processed after the response has finished streaming, enabling secondary tasks to run without blocking the primary response.

To use it, add experimental.after to next.config.js:

const nextConfig = {
  experimental: {
    after: true,
  },
};
 
module.exports = nextConfig;

Then, import the function in Server Components, Server Actions, Route Handlers, or Middleware.

import { unstable_after as after } from 'next/server';
import { log } from '@/app/utils';
 
export default function Layout({ children }) {
  // Secondary task
  after(() => {
    log();
  });
 
  // Primary task
  return <>{children}</>;
}

Learn more about next/after

.
create-next-app updates

For Next.js 15, we've updated create-next-app with a new design.
New design for create-next-app in Next.js 15 RC

When running create-next-app, there is a new prompt asking if you want to enable Turbopack for local development (defaults to No).

✔ Would you like to use Turbopack for next dev? … No / Yes

The --turbo flag can be used to enable Turbopack.

npx create-next-app@rc --turbo

To make getting started on a new project even easier, a new --empty flag has been added to the CLI. This will remove any extraneous files and styles, resulting in a minimal "hello world" page.

npx create-next-app@rc --empty

Optimizing bundling of external packages (Stable)

Bundling external packages can improve the cold start performance of your application. In the App Router, external packages are bundled by default, and you can opt-out specific packages using the new serverExternalPackages

config option.

In the Pages Router, external packages are not bundled by default, but you can provide a list of packages to bundle using the existing transpilePackages

option. With this configuration option, you need to specify each package.

To unify configuration between App and Pages Router, we’re introducing a new option, bundlePagesRouterDependencies
to match the default automatic bundling of the App Router. You can then use serverExternalPackages

to opt-out specific packages, if needed.

const nextConfig = {
  // Automatically bundle external packages in the Pages Router:
  bundlePagesRouterDependencies: true,
  // Opt specific packages out of bundling for both App and Pages Router:
  serverExternalPackages: ['package-name'],
};
 
module.exports = nextConfig;

Learn more about optimizing external packages

.
Other Changes

    [Breaking] Minimum React version is now 19 RC
    [Breaking] next/image: Removed squoosh in favor of sharp as an optional dependency (PR

)
[Breaking] next/image: Changed default Content-Disposition to attachment (PR
)
[Breaking] next/image: Error when src has leading or trailing spaces (PR
)
[Breaking] Middleware: Apply react-server condition to limit unrecommended react API imports (PR
)
[Breaking] next/font: Removed support for external @next/font package (PR
)
[Breaking] next/font: Removed font-family hashing (PR
)
[Breaking] Caching: force-dynamic will now set a no-store default to the fetch cache (PR
)
[Breaking] Config: Enable swcMinify (PR
), missingSuspenseWithCSRBailout (PR), and outputFileTracing (PR
) behavior by default and remove deprecated options
[Breaking] Remove auto-instrumentation for Speed Insights (must now use the dedicated @vercel/speed-insights
package) (PR
)
[Breaking] Remove .xml extension for dynamic sitemap routes and align sitemap URLs between development and production (PR
)
[Improvement] Metadata: Updated environmental variable fallbacks for metadataBase when hosted on Vercel (PR
)
[Improvement] Fix tree-shaking with mixed namespace and named imports from optimizePackageImports (PR
)
[Improvement] Parallel Routes: Provide unmatched catch-all routes with all known params (PR
)
[Improvement] Config bundlePagesExternals is now stable and renamed to bundlePagesRouterDependencies
[Improvement] Config serverComponentsExternalPackages is now stable and renamed to serverExternalPackages
[Improvement] create-next-app: New projects ignore all .env files by default (PR
)
[Docs] Improve auth documentation (PR
)
[Docs] @next/env package (PR
)
Nextjs 14:
Next.js 14
Lee Robinson@leeerobTim Neutkens@timneutkens
16–21 minutes

As we announced at Next.js Conf

, Next.js 14 is our most focused release with:

    Turbopack: 5,000 tests passing for App & Pages Router
        53% faster local server startup
        94% faster code updates with Fast Refresh
    Server Actions (Stable): Progressively enhanced mutations
        Integrated with caching & revalidating
        Simple function calls, or works natively with forms
    Partial Prerendering (Preview): Fast initial static response + streaming dynamic content
    Next.js Learn (New): Free course teaching the App Router, authentication, databases, and more.

Upgrade today or get started with:

npx create-next-app@latest

Next.js Compiler: Turbocharged

Since Next.js 13, we've been working to improve local development performance in Next.js in both the Pages and App Router.

Previously, we were rewriting next dev and other parts of Next.js to support this effort. We have since changed our approach to be more incremental. This means our Rust-based compiler will reach stability soon, as we've refocused on supporting all Next.js features first.

5,000 integration tests for next dev are now passing with Turbopack

, our underlying Rust engine. These tests include 7 years of bug fixes and reproductions.

While testing on vercel.com, a large Next.js application, we've seen:

    Up to 53.3% faster local server startup
    Up to 94.7% faster code updates with Fast Refresh

This benchmark is a practical result of performance improvements you should expect with a large application (and large module graph). With 90% of tests for next dev now passing, you should see faster and more reliable performance consistently when using next dev --turbo.

Once we hit 100% of tests passing, we'll move Turbopack to stable in an upcoming minor release. We'll also continue to support using webpack for custom configurations and ecosystem plugins.

You can follow the percentage of tests passing at areweturboyet.com

.
Forms and Mutations

Next.js 9 introduced API Routes—a way to quickly build backend endpoints alongside your frontend code.

For example, you would create a new file in the api/ directory:

import type { NextApiRequest, NextApiResponse } from 'next';
 
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  const data = req.body;
  const id = await createItem(data);
  res.status(200).json({ id });
}

Then, on the client-side, you could use React and an event handler like onSubmit to make a fetch to your API Route.

import { FormEvent } from 'react';
 
export default function Page() {
  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
 
    const formData = new FormData(event.currentTarget);
    const response = await fetch('/api/submit', {
      method: 'POST',
      body: formData,
    });
 
    // Handle response if necessary
    const data = await response.json();
    // ...
  }
 
  return (
    <form onSubmit={onSubmit}>
      <input type="text" name="name" />
      <button type="submit">Submit</button>
    </form>
  );
}

Now with Next.js 14, we want to simplify the developer experience of authoring data mutations. Further, we want to improve the user experience when the user has a slow network connection, or when submitting a form from a lower-powered device.
Server Actions (Stable)

What if you didn't need to manually create an API Route? Instead, you could define a function that runs securely on the server, called directly from your React components.

The App Router is built on the React canary channel, which is stable for frameworks

to adopt new features. As of v14, Next.js has upgraded to the latest React canary, which includes stable Server Actions.

The previous example from the Pages Router can be simplified to one file:

export default function Page() {
  async function create(formData: FormData) {
    'use server';
    const id = await createItem(formData);
  }
 
  return (
    <form action={create}>
      <input type="text" name="name" />
      <button type="submit">Submit</button>
    </form>
  );
}

Server Actions should feel familiar for any developers who have previously used server-centric frameworks in the past. It's built on web fundamentals like forms and the FormData Web API

.

While using Server Actions through a form is helpful for progressive enhancement, it is not a requirement. You can also call them directly as a function, without a form. When using TypeScript, this gives you full end-to-end type-safety between the client and server.

Mutating data, re-rendering the page, or redirecting can happen in one network roundtrip, ensuring the correct data is displayed on the client, even if the upstream provider is slow. Further, you can compose and reuse different actions, including many different actions in the same route.
Caching, Revalidating, Redirecting, and more

Server Actions are deeply integrated into the entire App Router model. You can:

    Revalidate cached data with revalidatePath() or revalidateTag()
    Redirect to different routes through redirect()
    Set and read cookies through cookies()
    Handle optimistic UI updates with useOptimistic()
    Catch and display errors from the server with useFormState()
    Display loading states on the client with useFormStatus()

Learn more about Forms and Mutations with Server Actions
or about the security model

and best practices for Server Components and Server Actions.
Partial Prerendering (Preview)

We'd like to share a preview of Partial Prerendering — a compiler optimization for dynamic content with a fast initial static response — that we're working on for Next.js.

Partial Prerendering builds on a decade of research and development into server-side rendering (SSR), static-site generation (SSG), and incremental static revalidation (ISR).
Motivation

We've heard your feedback. There's currently too many runtimes, configuration options, and rendering methods to have to consider. You want the speed and reliability of static, while also supporting fully dynamic, personalized responses.

Having great performance globally and personalization shouldn't come at the cost of complexity.

Our challenge was to create a better developer experience, simplifying the existing model without introducing new APIs for developers to learn. While partial caching of server-side content has existed, these approaches still need to meet the developer experience and composability goals we aim for.

Partial Prerendering requires no new APIs to learn.
Built on React Suspense

Partial Prerendering is defined by your Suspense boundaries. Here's how it works. Consider the following ecommerce page:

export default function Page() {
  return (
    <main>
      <header>
        <h1>My Store</h1>
        <Suspense fallback={<CartSkeleton />}>
          <ShoppingCart />
        </Suspense>
      </header>
      <Banner />
      <Suspense fallback={<ProductListSkeleton />}>
        <Recommendations />
      </Suspense>
      <NewProducts />
    </main>
  );
}

With Partial Prerendering enabled, this page generates a static shell based on your <Suspense /> boundaries. The fallback from React Suspense is prerendered.

Suspense fallbacks in the shell are then replaced with dynamic components, like reading cookies to determine the cart, or showing a banner based on the user.

When a request is made, the static HTML shell is immediately served:

<main>
  <header>
    <h1>My Store</h1>
    <div class="cart-skeleton">
      <!-- Hole -->
    </div>
  </header>
  <div class="banner" />
  <div class="product-list-skeleton">
    <!-- Hole -->
  </div>
  <section class="new-products" />
</main>

Since <ShoppingCart /> reads from cookies to look at the user session, this component is then streamed in as part of the same HTTP request as the static shell. There are no extra network roundtrips needed.

import { cookies } from 'next/headers'
 
export default function ShoppingCart() {
  const cookieStore = cookies()
  const session = cookieStore.get('session')
  return ...
}

To have the most granular static shell, this may require adding additional Suspense boundaries. However, if you're already using loading.js today, this is an implicit Suspense boundary, so no changes would be required to generate the static shell.
Coming soon

Partial prerendering is under active development. We'll be sharing more updates in an upcoming minor release.

Before your page content can be streamed from the server, there's important metadata about the viewport, color scheme, and theme that need to be sent to the browser first.

Ensuring these meta tags are sent with the initial page content helps a smooth user experience, preventing the page from flickering by changing the theme color, or shifting layout due to viewport changes.

In Next.js 14, we've decoupled blocking and non-blocking metadata. Only a small subset of metadata options are blocking, and we want to ensure non-blocking metadata will not prevent a partially prerendered page from serving the static shell.

The following metadata options are now deprecated and will be removed from metadata in a future major version:

    viewport: Sets the initial zoom and other properties of the viewport
    colorScheme: Sets the support modes (light/dark) for the viewport
    themeColor: Sets the color the chrome around the viewport should render with

Starting with Next.js 14, there are new options viewport and generateViewport

to replace these options. All other metadata options remain the same.

You can start adopting these new APIs today. The existing metadata options will continue to work.
Next.js Learn Course

Today we're releasing a brand new, free course on Next.js Learn

. This course teaches:

    The Next.js App Router
    Styling and Tailwind CSS
    Optimizing Fonts and Images
    Creating Layouts and Pages
    Navigating Between Pages
    Setting Up Your Postgres Database
    Fetching Data with Server Components
    Static and Dynamic Rendering
    Streaming
    Partial Prerendering (Optional)
    Adding Search and Pagination
    Mutating Data
    Handling Errors
    Improving Accessibility
    Adding Authentication
    Adding Metadata

Next.js Learn has taught millions of developers about the foundations of the framework, and we can't wait to hear your feedback on our new addition. Head to nextjs.org/learn

to take the course.
Other Changes

    [Breaking] Minimum Node.js version is now 18.17
    [Breaking] Removes WASM target for next-swc build (PR

)
[Breaking] Dropped support for @next/font in favor of next/font (Codemod
)
[Breaking] Changed ImageResponse import from next/server to next/og (Codemod
)
[Breaking] next export command has been removed in favor of output: 'export' config (Docs
)
[Deprecation] onLoadingComplete for next/image is deprecated in favor of onLoad
[Deprecation] domains for next/image is deprecated in favor of remotePatterns[Feature] More verbose logging around fetch caching can be enabled (Docs
)
[Improvement] 80% smaller function size for a basic create-next-app application
[Improvement] Enhanced memory management when using edge runtime in development

ProductList.jsx:
'use client'
import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
import { useInView } from 'react-intersection-observer';
import dynamic from 'next/dynamic';
import ProductItem from './ProductItem';
import ProductLoading from '@/components/ProductLoading';
import NoProductsFound from '@/components/NoProductsFound';
import ProductDetail from '@/components/ProductDetail';
import { NotificationProvider } from "@/components/NotificationContext";

const SearchComponent = dynamic(() => import('@/components/SearchComponent'), {
    ssr: false,
    loading: () => <SearchComponentSkeleton />
});

const ProductModal = dynamic(() => import("@/components/ProductDetailModal"), {
    ssr: false
});

export default function ProductList({
                                        initialProducts,
                                        hasMore,
                                        initialPage,
                                        initialQuery,
                                        minPrice,
                                        maxPrice,
                                        title
                                    }) {
    const [products, setProducts] = useState(initialProducts);
    const [page, setPage] = useState(initialPage);
    const [loading, setLoading] = useState(false);
    const [noMoreProducts, setNoMoreProducts] = useState(!hasMore);
    const [query, setQuery] = useState(initialQuery);
    const [detail, setDetail] = useState(null);
    const [selectedProduct, setSelectedProduct] = useState(null);
    const [shouldScroll, setShouldScroll] = useState(false);
    const scroll = useRef(0);

    const { ref, inView } = useInView({ threshold: 0 });
    const router = useRouter();
    const searchParams = useSearchParams();
    const pathname = usePathname();
    const loadMoreProducts = useCallback(() => {
        if (loading || noMoreProducts) return;

        setLoading(true);
        const nextPage = page + 1;
        const params = new URLSearchParams(Array.from(searchParams.entries()));
        params.set('page', nextPage.toString());
        router.push(`/products?${params.toString()}`, { scroll: false });
    }, [page, loading, noMoreProducts, searchParams, router]);

    useEffect(() => {
        if (inView && !noMoreProducts) {
            loadMoreProducts();
        }
    }, [inView, loadMoreProducts, noMoreProducts]);

    const handleSearch = useCallback((newQuery) => {
        setQuery(newQuery);
        const params = new URLSearchParams(Array.from(searchParams.entries()));
        params.set('query', newQuery);
        params.set('page', '1');
        router.push(`/products?${params.toString()}`, { scroll: false });
    }, [searchParams, router]);

    useEffect(() => {
        setProducts(initialProducts);
        setPage(initialPage);
        setNoMoreProducts(!hasMore);
        setQuery(initialQuery);
        setLoading(false);
    }, [initialProducts, initialPage, hasMore, initialQuery]);

    const memoizedProducts = useMemo(() => products.map((product) => ({
        ...product,
        cheapestPrice: getCheapestPrice(product),
    })), [products]);

    useEffect(() => {
        if (shouldScroll) {
            window.scrollTo(0, scroll.current);
            setShouldScroll(false);
        }
    }, [shouldScroll]);

    useLayoutEffect(() => {
        if (pathname === '/products') {
            setDetail(null);
            setShouldScroll(true);
        }
    }, [pathname]);

    const handleProductClick = useCallback((product) => {
        scroll.current = window.scrollY;
        setDetail(product);
        const newURL = `/products/${product.id}`;
        window.history.pushState({ path: newURL }, '', newURL);
    }, []);

    const handleCartClick = useCallback((product) => {
        setSelectedProduct(product);
    }, []);

    const handleModalClose = useCallback(() => {
        setSelectedProduct(null);
    }, []);

    return (
        <>
            {detail ? (
                <NotificationProvider>
                    <ProductDetail product={detail} />
                </NotificationProvider>
            ) : (
                <>
                    <header className="flex justify-center relative mt-20 mb-4">
                        <h1
                            className="text-3xl z-10 text-[#181717] font-sans text-center font-medium">
                            {title || 'All Products'}
                        </h1>
                    </header>
                    <SearchComponent minPrice={minPrice} maxPrice={maxPrice} query={query} setQuery={handleSearch}/>
                    {memoizedProducts.length > 0 ? (
                        <div
                            className="grid grid-cols-2 w-full justify-between gap-4 sm:gap-6 ssm3:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
                            {memoizedProducts.map((product) => (
                                <ProductItem
                                    key={product.id}
                                    id={product.id}
                                    name={product.name}
                                    product={product}
                                    handleClick={() => handleProductClick(product)}
                                    price={formatPrice(product.cheapestPrice)}
                                    imageUrl={product.images[0]}
                                    onCartClick={handleCartClick}
                                />
                            ))}
                        </div>
                    ) : (
                        <NoProductsFound />
                    )}
                    {loading && <ProductLoading />}
                    {!loading && !noMoreProducts && <div ref={ref} style={{height: '20px'}}></div>}
                </>
            )}
            {selectedProduct && (
                <NotificationProvider>
                    <ProductModal
                        productData={{ product: selectedProduct }}
                        isOpen={!!selectedProduct}
                        onClose={handleModalClose}
                    />
                </NotificationProvider>
            )}
        </>
    );
}

const getCheapestPrice = (product) => {
    const prices = [
        product.price && product.price !== '0.00'
            ? parseFloat(product.price)
            : Infinity,
        ...(product.sizes?.map((size) => parseFloat(size.price)) || []),
        ...(product.colors?.flatMap((color) => [
            parseFloat(color.price),
            ...(color.sizes?.map((size) => parseFloat(size.price)) || []),
        ]) || []),
    ].filter((price) => !isNaN(price) && isFinite(price));

    return prices.length > 0 ? Math.min(...prices) : null;
};

const formatPrice = (price) => {
    if (price == null || price === Infinity) return 'N/A';
    return `${price >= 10000 ? price.toLocaleString() : price} IQD`;
};

const SearchComponentSkeleton = () => (
    <div className="flex mb-4 items-center space-x-2 animate-pulse">
        <div className="h-10 bg-gray-200 rounded-md flex-grow"></div>
        <div className="h-10 w-[5.5rem] bg-gray-200 rounded-md"></div>
    </div>
);
products/page.jsx:
import React from 'react';
import ProductList from '@/components/ProductList';
import AsyncNavBar from '@/components/AsyncNavBar';
import { unstable_cache } from 'next/cache';
import Link from 'next/link';

const ITEMS_PER_PAGE = 15;
export const dynamic = 'force-dynamic'

const REVALIDATE_SUBBRANDS = 14400; // 4 hours
const REVALIDATE_PRODUCTS = 0; // 4 hours

// Wrap fetchSubBrands with unstable_cache
const fetchSubBrands = unstable_cache(
    async () => {
        const response = await fetch('https://api.naayiq.com/subcategories/brands', {
            headers: { 'Content-Type': 'application/json' },
            cache: "force-cache",
            next: {revalidate: REVALIDATE_SUBBRANDS}
        });

        if (response.status === 200) {
            return response.json();
        }
        throw new Error(`Failed to fetch sub-brands: ${response.statusText}`);
    },
    {
        revalidate: REVALIDATE_SUBBRANDS,
    }
);

// Wrap fetchProducts with unstable_cache
const fetchProducts = async () => {
    const response = await fetch('https://api.naayiq.com/products', {
        headers: {'Content-Type': 'application/json'},
        next: {revalidate: REVALIDATE_PRODUCTS}
    });

    if (!response.ok) {
        throw new Error('Failed to fetch products');
    }
    return response.json();
}

// Cache getFilteredProducts to ensure it's only processed once per request
const getFilteredProducts = async (
    page = 1,
    query = '',
    c = '',
    sc = '',
    b = '',
    minPriceA = 0,
    maxPriceA = Infinity,
    availability = 'all',
    sortBy = ''
) => {
    const [productsData, subBrands] = await Promise.all([
        fetchProducts(),
        b ? fetchSubBrands() : Promise.resolve([]), // Uses unstable_cache: fetched once every 4 hours if 'b' is truthy
    ]);

    let products = productsData.products;

    const subCategories = b
        ? subBrands
            .filter((item) => item.category_id.toString() === b)
            .map((item) => item.id.toString())
        : [];

    // Calculate min and max prices
    let minPrice = Infinity;
    let maxPrice = -Infinity;
    products.forEach((product) => {
        product.sizes.forEach((size) => {
            const price = parseFloat(size.price);
            if (price < minPrice) minPrice = price;
            if (price > maxPrice) maxPrice = price;
        });
    });

    const filterProduct = (product) => {
        if (
            c &&
            !product.categories.some((cat) => cat.main_category_id?.toString() === c)
        )
            return false;
        if (
            sc &&
            product?.brand_id.toString() !== sc &&
            !product.categories.some((cat) => cat.id?.toString() === sc)
        )
            return false;
        if (b && !subCategories.includes(product?.brand_id.toString())) return false;

        // Price range filter
        const productPrices = product.sizes.map((size) => parseFloat(size.price));
        const productMinPrice = Math.min(...productPrices);
        const productMaxPrice = Math.max(...productPrices);

        if (productMinPrice < minPriceA || productMaxPrice > maxPriceA) {
            return false;
        }

        // Availability filter
        if (availability !== 'all') {
            const inStock = product.sizes.some((size) => size.qty > 0);
            if (
                (availability === 'in_stock' && !inStock) ||
                (availability === 'out_of_stock' && inStock)
            ) {
                return false;
            }
        }

        // Query filter
        if (query) {
            const normalizedName = product.name.normalize('NFC').toLowerCase();
            const cleanName = normalizedName.replace(
                /[\u200E\u200F\u202A-\u202E\u2066-\u2069]/g,
                ''
            );
            const queryWords = query.toLowerCase().split(/\s+/);
            return queryWords.every((word) => cleanName.includes(word));
        }

        return true;
    };

    // Apply the filter
    let filteredProducts = products.filter(filterProduct);

    // Calculate total sold for each product
    filteredProducts.forEach((product) => {
        product.totalSold = product.sizes.reduce((sum, size) => sum + size.sold, 0);
    });

    // Apply sorting
    switch (sortBy) {
        case 'Price: Low to High':
            filteredProducts.sort(
                (a, b) =>
                    Math.min(...a.sizes.map((s) => parseFloat(s.price))) -
                    Math.min(...b.sizes.map((s) => parseFloat(s.price)))
            );
            break;
        case 'Price: High to Low':
            filteredProducts.sort(
                (a, b) =>
                    Math.max(...b.sizes.map((s) => parseFloat(s.price))) -
                    Math.max(...a.sizes.map((s) => parseFloat(s.price)))
            );
            break;
        case 'Name: A to Z':
            filteredProducts.sort((a, b) => a.name.localeCompare(b.name));
            break;
        case 'Name: Z to A':
            filteredProducts.sort((a, b) => b.name.localeCompare(a.name));
            break;
        case 'Best Selling':
            filteredProducts.sort((a, b) => b.totalSold - a.totalSold);
            break;
        case 'Newest Arrivals':
            filteredProducts.sort(
                (a, b) => new Date(b.created_at) - new Date(a.created_at)
            );
            break;
        default:
            // Default sorting (optional)
            break;
    }

    // Additional query-based sorting if applicable
    if (query && !sortBy) {
        const lowerQuery = query.toLowerCase();
        filteredProducts.sort((a, b) => {
            const aIndex = a.name.toLowerCase().indexOf(lowerQuery);
            const bIndex = b.name.toLowerCase().indexOf(lowerQuery);
            return (aIndex === -1 ? Infinity : aIndex) - (bIndex === -1 ? Infinity : bIndex);
        });
    }

    const totalProducts = filteredProducts.length;
    const end = page * ITEMS_PER_PAGE;

    return {
        products: filteredProducts.slice(0, end),
        totalProducts,
        hasMore: end < totalProducts,
        minPrice,
        maxPrice,
    };
};

// The main ProductsPage component
const ProductsPage = async ({ searchParams }) => {
    // Destructure and parse search parameters
    const {
        page = '1',
        query = '',
        c = '',
        sc = '',
        b = '',
        minPrice = '0',
        maxPrice = '',
        availability = 'all',
        sortBy = '',
        title = '',
    } = searchParams;

    const pageNum = parseInt(page, 10) || 1;
    const minPriceNum = parseFloat(minPrice) || 0;
    const maxPriceNum = maxPrice ? parseFloat(maxPrice) : Infinity;

    try {
        // Get filtered products based on search parameters
        const { products, totalProducts, hasMore, minPrice, maxPrice } = await getFilteredProducts(
            pageNum,
            query,
            c,
            sc,
            b,
            minPriceNum,
            maxPriceNum,
            availability,
            sortBy
        );

        return (
            <>
                <AsyncNavBar />
                <ProductList
                    initialProducts={products}
                    totalProducts={totalProducts}
                    hasMore={hasMore}
                    initialPage={pageNum}
                    initialQuery={query}
                    initialC={c}
                    initialSc={sc}
                    initialB={b}
                    minPrice={minPrice}
                    maxPrice={maxPrice}
                    title={sortBy || title || 'All Products'}
                />
            </>
        );
    } catch (error) {
        // Enhanced error handling with user-friendly message
        console.error('Error fetching products:', error);
        return (
            <div className="font-serif flex flex-col justify-center items-center p-4">
                <p>Oops! Something went wrong while loading products. Please try again later.</p>
                <Link href="/" className="text-blue-500 underline mt-2">
                    Go back home
                </Link>
            </div>
        );
    }
};

export default ProductsPage;