diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml
index 1793cab7..92a89982 100644
--- a/.github/workflows/e2e-tests.yml
+++ b/.github/workflows/e2e-tests.yml
@@ -19,7 +19,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
- node-version: 16.x
+ node-version: 18.x
- uses: pnpm/action-setup@v2.2.2
with:
diff --git a/README.md b/README.md
index ee4a5ac2..e743b11c 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,7 @@ To cache our server-rendered pages at the Fastly layer, we use response headers
1. `Surrogate-Control` response header needs to be added to pages where caching is desired ([reference](https://docs.fastly.com/en/guides/working-with-surrogate-keys)),
2. `Surrogate-Key` response header needs to be added to enable appropriate cache invalidation ([reference](https://developer.fastly.com/reference/api/purging/)).
-On the Next.js side we'll need to include a few primary response headers to then control caching (in our case, we're setting these headers from `getServerSideProps` on server-rendered pages that we'd like to cache).
+On the Next.js side we'll need to include a few primary response headers to then control caching (in our case, we're setting these headers from `middleware` on server-rendered pages that we'd like to cache).
- `surrogate-control` – Fastly-specific header used to set the cache policies. (`max-age`, `stale-while-revalidate`, `stale-while-error`).
- `surrogate-key` – Fastly-specific header that allows purging by key. Note: this header is removed by Fastly before sending the response to the client. To see the value of this header, you must include the [`Fastly-Debug`](https://developer.fastly.com/reference/http/http-headers/Fastly-Debug/) header in your request.
diff --git a/packages/nextjs/app/about/page.tsx b/packages/nextjs/app/about/page.tsx
new file mode 100644
index 00000000..6b0b1666
--- /dev/null
+++ b/packages/nextjs/app/about/page.tsx
@@ -0,0 +1,5 @@
+import AboutPage from "app/migration/about";
+
+export default async function Page() {
+ return ;
+}
diff --git a/packages/nextjs/app/categories/page.tsx b/packages/nextjs/app/categories/page.tsx
new file mode 100644
index 00000000..78b9ab2a
--- /dev/null
+++ b/packages/nextjs/app/categories/page.tsx
@@ -0,0 +1,19 @@
+import CategoriesPage from "app/migration/categories";
+import { getAllCategories } from "utils/getAllCategoriesQuery";
+import { isString, pluralize } from "utils/pluralize";
+
+const getData = async () => {
+ const categories = await getAllCategories();
+ const categoryNames = pluralize((categories || []).map((cat) => cat.name).filter(isString));
+
+ return {
+ categories,
+ categoryNames,
+ };
+};
+
+export default async function Page() {
+ const data = await getData();
+
+ return ;
+}
diff --git a/packages/nextjs/app/global.css b/packages/nextjs/app/global.css
index 60925395..8c7d56cd 100644
--- a/packages/nextjs/app/global.css
+++ b/packages/nextjs/app/global.css
@@ -1,3 +1,85 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
body {
- margin: 0;
+ @apply bg-secondary;
+}
+
+/* Cabinet Grotesk */
+/* See: https://api.fontshare.com/v2/css?f[]=cabinet-grotesk@500,700,400,300&display=swap */
+/* See: https://nextjs.org/docs/app/building-your-application/styling/css-modules#external-stylesheets */
+@font-face {
+ font-family: 'Cabinet Grotesk';
+ src: url('//cdn.fontshare.com/wf/7GWNQ5AHAZORLOWZ7ELKPLOIQITAR5S5/NYLYMGXMB4RANWVNJSIHG2IKPZ44CN5E/MT4CWVHB3N2C6KFUZ75QK4JQ2FYK4J4M.woff2') format('woff2'),
+ url('//cdn.fontshare.com/wf/7GWNQ5AHAZORLOWZ7ELKPLOIQITAR5S5/NYLYMGXMB4RANWVNJSIHG2IKPZ44CN5E/MT4CWVHB3N2C6KFUZ75QK4JQ2FYK4J4M.woff') format('woff'),
+ url('//cdn.fontshare.com/wf/7GWNQ5AHAZORLOWZ7ELKPLOIQITAR5S5/NYLYMGXMB4RANWVNJSIHG2IKPZ44CN5E/MT4CWVHB3N2C6KFUZ75QK4JQ2FYK4J4M.ttf') format('truetype');
+ font-weight: 300;
+ font-display: swap;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Cabinet Grotesk';
+ src: url('//cdn.fontshare.com/wf/J6PPRPKWXDUIYA47IXLEQB4R4OPVYDQH/N2ZXAXWEHVMLISD2TIXJC7EF4GOY43L4/NXM4Z4TDCMYWBZ7AVI2N6DQ5VMWNENMU.woff2') format('woff2'),
+ url('//cdn.fontshare.com/wf/J6PPRPKWXDUIYA47IXLEQB4R4OPVYDQH/N2ZXAXWEHVMLISD2TIXJC7EF4GOY43L4/NXM4Z4TDCMYWBZ7AVI2N6DQ5VMWNENMU.woff') format('woff'),
+ url('//cdn.fontshare.com/wf/J6PPRPKWXDUIYA47IXLEQB4R4OPVYDQH/N2ZXAXWEHVMLISD2TIXJC7EF4GOY43L4/NXM4Z4TDCMYWBZ7AVI2N6DQ5VMWNENMU.ttf') format('truetype');
+ font-weight: 400;
+ font-display: swap;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Cabinet Grotesk';
+ src: url('//cdn.fontshare.com/wf/CKQBK2QBTCDREE7L3MXZ3PPW7LDNJCWU/OTOY7FQFSFOJVZKJWKO2EHUJLOGBDN4Q/4CO2ETY7NITKLUDKMYJ75RHJSPHOJ7XT.woff2') format('woff2'),
+ url('//cdn.fontshare.com/wf/CKQBK2QBTCDREE7L3MXZ3PPW7LDNJCWU/OTOY7FQFSFOJVZKJWKO2EHUJLOGBDN4Q/4CO2ETY7NITKLUDKMYJ75RHJSPHOJ7XT.woff') format('woff'),
+ url('//cdn.fontshare.com/wf/CKQBK2QBTCDREE7L3MXZ3PPW7LDNJCWU/OTOY7FQFSFOJVZKJWKO2EHUJLOGBDN4Q/4CO2ETY7NITKLUDKMYJ75RHJSPHOJ7XT.ttf') format('truetype');
+ font-weight: 500;
+ font-display: swap;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Cabinet Grotesk';
+ src: url('//cdn.fontshare.com/wf/XMXWOHABYLQDJ42L65EFRYNVRY37HQCB/B2O4O6V3JMFM2WDCYQI3A47L5U4THDUL/WN5274VQ3AUBDFP74GB4EC4XYJ3EKVNE.woff2') format('woff2'),
+ url('//cdn.fontshare.com/wf/XMXWOHABYLQDJ42L65EFRYNVRY37HQCB/B2O4O6V3JMFM2WDCYQI3A47L5U4THDUL/WN5274VQ3AUBDFP74GB4EC4XYJ3EKVNE.woff') format('woff'),
+ url('//cdn.fontshare.com/wf/XMXWOHABYLQDJ42L65EFRYNVRY37HQCB/B2O4O6V3JMFM2WDCYQI3A47L5U4THDUL/WN5274VQ3AUBDFP74GB4EC4XYJ3EKVNE.ttf') format('truetype');
+ font-weight: 700;
+ font-display: swap;
+ font-style: normal;
+}
+
+
+@font-face {
+ font-family: "JeanLuc";
+ src: url('../assets/fonts/jeanluc/jeanlucweb-bold.woff');
+ font-weight: bold;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "JeanLuc";
+ src: url('../assets/fonts/jeanluc/jeanlucweb-thin.woff');
+ font-weight: auto;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "JetBrains Mono";
+ src: url('../assets/fonts/jetbrainsmono/JetBrainsMono-Bold.woff2');
+ font-weight: bold;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "JetBrains Mono";
+ src: url('../assets/fonts/jetbrainsmono/JetBrainsMono-Regular.woff2');
+ font-weight: auto;
+ font-style: normal;
+}
+
+code {
+ @apply rounded-md bg-black/5 px-1.5 py-0.5;
+ font-family: 'JetBrains Mono', monospace;
+ font-size: 0.9rem;
}
\ No newline at end of file
diff --git a/packages/nextjs/app/layout.tsx b/packages/nextjs/app/layout.tsx
index 0f479fcc..205793b2 100644
--- a/packages/nextjs/app/layout.tsx
+++ b/packages/nextjs/app/layout.tsx
@@ -1,9 +1,39 @@
+import { Footer, MobileNavProvider } from "./ui/shared-ui";
import "./global.css";
+import { Header } from "components/Header/Header";
+import { Metadata } from "next";
+import { CartProvider } from "components/CartContext";
+import { AnimatePresence, MotionConfig } from "./ui/framer";
+
+export const metadata: Metadata = {
+ title: "Home",
+ description: "Welcome to Next.js",
+};
+
+export const viewport = {
+ width: "device-width",
+};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
-
{children}
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
);
}
diff --git a/packages/nextjs/pages/about.tsx b/packages/nextjs/app/migration/about.tsx
similarity index 98%
rename from packages/nextjs/pages/about.tsx
rename to packages/nextjs/app/migration/about.tsx
index 94fa38b1..7a37ef49 100644
--- a/packages/nextjs/pages/about.tsx
+++ b/packages/nextjs/app/migration/about.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import * as React from "react";
import { NextPage } from "next";
import NextImage from "next/legacy/image";
@@ -58,7 +60,7 @@ const AboutPage: NextPage = () => {
{
{
On the Next.js side we’ll need to include a few primary response headers to then control caching (in
- our case, we’re setting these headers from
getServerSideProps
on server-rendered pages
- that we’d like to cache).
+ our case, we’re setting these headers from
middleware
on server-rendered pages that
+ we’d like to cache).
diff --git a/packages/nextjs/pages/categories.tsx b/packages/nextjs/app/migration/categories.tsx
similarity index 57%
rename from packages/nextjs/pages/categories.tsx
rename to packages/nextjs/app/migration/categories.tsx
index a9921974..2694bae5 100644
--- a/packages/nextjs/pages/categories.tsx
+++ b/packages/nextjs/app/migration/categories.tsx
@@ -1,10 +1,8 @@
-import { GetServerSideProps, NextPage } from "next";
+"use client";
+
+import { NextPage } from "next";
import { WeDontSellBreadBanner } from "shared-ui";
-import { setCachingHeaders } from "utils/setCachingHeaders";
-import { SanityType } from "utils/consts";
-import { isString, pluralize } from "utils/pluralize";
-import { getAllCategories } from "utils/getAllCategoriesQuery";
import { CategoryList } from "components/CategoryList";
import { PageHead } from "components/PageHead";
@@ -34,18 +32,4 @@ const CategoriesPage: NextPage
= ({ categories, categoryNames }) => {
);
};
-export const getServerSideProps = (async ({ res }) => {
- setCachingHeaders(res, [SanityType.Category, SanityType.CategoryImage]);
-
- const categories = await getAllCategories();
- const categoryNames = pluralize((categories || []).map((cat) => cat.name).filter(isString));
-
- return {
- props: {
- categories,
- categoryNames,
- },
- };
-}) satisfies GetServerSideProps;
-
export default CategoriesPage;
diff --git a/packages/nextjs/pages/components.tsx b/packages/nextjs/app/migration/components.tsx
similarity index 98%
rename from packages/nextjs/pages/components.tsx
rename to packages/nextjs/app/migration/components.tsx
index 5559baa6..d86ad762 100644
--- a/packages/nextjs/pages/components.tsx
+++ b/packages/nextjs/app/migration/components.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import { MdArrowForward } from "react-icons/md";
import { Button, Input, Pill, Checkbox, Select, LinkText } from "shared-ui";
@@ -36,6 +38,7 @@ export default function ComponentsPage() {
Select
{
+ const { [SORT_QUERY_PARAM]: sortValue } = query;
+
+ // Sort/ordering
+ let ordering = SORT_OPTIONS.default.ordering;
+ if (sortValue) {
+ // If sort is string[], use first item
+ // (e.g. User modified url, wouldn't happen normally)
+ const sortType = Array.isArray(sortValue) ? sortValue[0] : sortValue;
+ const sortOption = SORT_OPTIONS[sortType];
+ if (sortOption?.ordering) {
+ ordering = sortOption.ordering;
+ }
+ }
+
+ return ordering;
+};
diff --git a/packages/nextjs/pages/index.tsx b/packages/nextjs/app/migration/home-page.tsx
similarity index 77%
rename from packages/nextjs/pages/index.tsx
rename to packages/nextjs/app/migration/home-page.tsx
index eb848b02..645d6b15 100644
--- a/packages/nextjs/pages/index.tsx
+++ b/packages/nextjs/app/migration/home-page.tsx
@@ -1,21 +1,18 @@
+"use client";
+
import type { Categories, Products } from "utils/groqTypes/ProductList";
import * as React from "react";
-import { GetServerSideProps, NextPage } from "next";
+import { NextPage } from "next";
import { FiArrowRight } from "react-icons/fi";
import Link from "next/link";
import NextImage from "next/legacy/image";
-import { Button, FeaturedQuote } from "shared-ui";
-import { setCachingHeaders } from "utils/setCachingHeaders";
+import { Button, FeaturedQuote } from "../ui/shared-ui";
import { localImageLoader } from "utils/localImageLoader";
-import { SanityType } from "utils/consts";
-import { getAllCategories } from "utils/getAllCategoriesQuery";
-import { getRecommendations } from "utils/getRecommendationsQuery";
import featuredImg from "assets/featured-story.jpg";
import { FeaturedList } from "components/FeaturedList";
import { Image } from "components/Image";
-import { PageHead } from "components/PageHead";
interface PageProps {
data?: {
@@ -27,10 +24,6 @@ interface PageProps {
const Home: NextPage = ({ data }) => {
return (
<>
-
@@ -101,20 +94,4 @@ const TitleBanner = ({ children }: React.PropsWithChildren) => (
);
-export const getServerSideProps = (async ({ res }) => {
- setCachingHeaders(res, [SanityType.Category, SanityType.CategoryImage]);
-
- const categories = await getAllCategories();
- const products = await getRecommendations();
-
- return {
- props: {
- data: {
- products,
- categories,
- },
- },
- };
-}) satisfies GetServerSideProps
;
-
export default Home;
diff --git a/packages/nextjs/pages/products/[slug].tsx b/packages/nextjs/app/migration/products/[slug].tsx
similarity index 72%
rename from packages/nextjs/pages/products/[slug].tsx
rename to packages/nextjs/app/migration/products/[slug].tsx
index 6b85fc8d..389c7bd7 100644
--- a/packages/nextjs/pages/products/[slug].tsx
+++ b/packages/nextjs/app/migration/products/[slug].tsx
@@ -1,16 +1,12 @@
-import type { GetProductsAndCategoriesQuery, Product as ProductType, Variant } from "utils/groqTypes/ProductList";
+"use client";
+
import * as React from "react";
import { useState } from "react";
-import { GetServerSideProps, NextPage } from "next";
-import { useRouter } from "next/router";
+import { NextPage } from "next";
import { AnimatePresence } from "framer-motion";
import { H6, FadeInOut, BlockContent, Price, QuantityInput, useCart } from "shared-ui";
-import { setCachingHeaders } from "utils/setCachingHeaders";
-import { isSlug } from "utils/isSlug";
-import { SanityType } from "utils/consts";
import { getRecommendations } from "utils/getRecommendationsQuery";
-import { getProductBySlug } from "utils/getProductBySlug";
import { ImageCarousel } from "components/ImageCarousel";
import { PageHead } from "components/PageHead";
@@ -18,20 +14,23 @@ import { StyleOptions } from "components/ProductPage/StyleOptions";
import { ProductVariantSelector } from "components/ProductPage/ProductVariantSelector";
import { Product } from "components/Product";
import { Breadcrumbs } from "components/Breadcrumbs";
+import { useSearchParams, useRouter } from "next/navigation";
+import { ProductDetail, ProductDetailVariants } from "utils/groqTypes/ProductDetail";
interface PageProps {
data?: {
- products: GetProductsAndCategoriesQuery["products"];
- recommendations: GetProductsAndCategoriesQuery["products"];
+ product: ProductDetail;
+ recommendations: Awaited>;
};
}
const ProductPage: NextPage = ({ data }) => {
- const { query } = useRouter();
+ const query = useSearchParams();
+ const product = data?.product;
+ const variant = query?.get("variant");
- const product = data?.products[0];
const selectedVariant =
- (product?.variants || []).find((v) => v?.slug && v.slug === query.variant) || product?.variants?.[0];
+ (product?.variants || []).find((v) => v?.slug && v.slug === variant) || product?.variants?.[0];
return (
@@ -41,7 +40,7 @@ const ProductPage: NextPage = ({ data }) => {
-
+
@@ -75,18 +74,13 @@ const ProductPage: NextPage = ({ data }) => {
);
};
-const PageBody = ({ variant, product }: { product?: ProductType; variant?: Variant }) => {
+const PageBody = ({ variant, product }: { product?: ProductDetail; variant?: ProductDetailVariants[number] }) => {
const { replace } = useRouter();
const { updateCart, cartItems } = useCart();
const setSelectedVariant = React.useCallback(
(slug: string) => {
- replace({
- pathname: window.location.pathname,
- query: {
- variant: slug,
- },
- }).catch(() => null);
+ replace(`${window.location.pathname}?variant=${slug}`);
},
[replace]
);
@@ -149,30 +143,4 @@ const PageBody = ({ variant, product }: { product?: ProductType; variant?: Varia
);
};
-export const getServerSideProps = (async ({ res, query }) => {
- const { slug } = query;
-
- const cacheKeys = [] as string[];
- if (isSlug(slug)) {
- cacheKeys.push(`${SanityType.Product}_${slug}`);
- }
-
- const products = await getProductBySlug(isSlug(slug) ? slug : "");
- const recommendations = await getRecommendations();
-
- // Extract variant slugs to add to cache keys, in case any of those change.
- const variantSlugs: string[] = (products[0]?.variants?.map((v: any) => v?.slug) || []).filter(Boolean);
- cacheKeys.push(...variantSlugs.map((s) => `${SanityType.Variant}_${s}`));
- setCachingHeaders(res, cacheKeys);
-
- return {
- props: {
- data: {
- products,
- recommendations,
- },
- },
- };
-}) satisfies GetServerSideProps;
-
export default ProductPage;
diff --git a/packages/nextjs/pages/products/index.tsx b/packages/nextjs/app/migration/products/index.tsx
similarity index 65%
rename from packages/nextjs/pages/products/index.tsx
rename to packages/nextjs/app/migration/products/index.tsx
index 7095c43e..b8ea69aa 100644
--- a/packages/nextjs/pages/products/index.tsx
+++ b/packages/nextjs/app/migration/products/index.tsx
@@ -1,17 +1,11 @@
-import { GetServerSideProps, GetServerSidePropsResult, NextPage } from "next";
+"use client";
+
+import { NextPage } from "next";
import * as React from "react";
import { AnimatePresence } from "framer-motion";
-import { useRouter } from "next/router";
import classNames from "classnames";
import { H6, WeDontSellBreadBanner, FadeInOut } from "shared-ui";
-import { getAllFilteredVariants } from "utils/getFilteredPaginatedQuery";
-import { getCategoryFilters, getFlavourFilters, getStyleFilters } from "utils/getFilters";
-import { getPaginationFromQuery } from "utils/getPaginationFromQuery";
-import { getFiltersFromQuery } from "utils/getFiltersFromQuery";
-import { getOrderingFromQuery } from "shared-ui";
-import { setCachingHeaders } from "utils/setCachingHeaders";
-import { SanityType } from "utils/consts";
import { pluralize } from "utils/pluralize";
import { CategoryFilterItem, FlavourFilterItem, PLPVariant, StyleFilterItem } from "utils/groqTypes/ProductList";
import { useDeviceSize } from "utils/useDeviceSize";
@@ -24,6 +18,7 @@ import { Pagination } from "components/Pagination";
import { Breadcrumbs } from "components/Breadcrumbs";
import { ModalFiltersMobile } from "views/ModalFiltersMobile";
import { SortAndFiltersToolbarMobile } from "views/SortAndFiltersToolbarMobile";
+import { useSearchParams } from "next/navigation";
interface ProductsPageProps {
variants: PLPVariant[];
@@ -45,7 +40,7 @@ const ProductsPage: NextPage = ({
styleFilters,
}) => {
const productNames = pluralize(variants.map((prod) => prod.name));
- const { query } = useRouter();
+ const query = useSearchParams();
const [isModalOpen, setIsModalOpen] = React.useState(false);
const { isSm } = useDeviceSize();
@@ -103,7 +98,7 @@ const ProductsPage: NextPage = ({
1 && "grid-rows-2"
+ +(query?.get("page") || 1) > 1 && "grid-rows-2"
)}
key={productNames}
>
@@ -111,7 +106,7 @@ const ProductsPage: NextPage = ({
))}
{/* Add padder items when on page > 1 so pagination bar isn't moving around */}
- {+(query?.page || 1) > 1 &&
+ {+(query?.get("page") || 1) > 1 &&
Array.from({ length: 6 - variants.length })
.fill(undefined)
.map((_, i) =>
)}
@@ -142,55 +137,4 @@ const ProductsPage: NextPage = ({
);
};
-export const getServerSideProps = (async ({ query, res, resolvedUrl }) => {
- setCachingHeaders(res, [SanityType.Product, SanityType.Style, SanityType.Flavour, SanityType.Variant]);
-
- // Sort/ordering.
- const order = getOrderingFromQuery(query);
-
- // Fetch size filters from sanity
- const categoryFilters = await getCategoryFilters();
- const flavourFilters = await getFlavourFilters();
- const styleFilters = await getStyleFilters();
-
- // Filters.
- const filters = getFiltersFromQuery(query, { flavourFilters, styleFilters, categoryFilters });
- // Pagination data.
- const pagination = getPaginationFromQuery(query);
- const result = await getAllFilteredVariants(filters, order, pagination);
-
- const { variants, itemCount } = result;
- const { currentPage, pageSize } = pagination;
- const pageCount = Math.ceil(itemCount / pageSize);
-
- /**
- * Scenario: If user is on the third page and then enables
- * a filter that only returns two pages worth of products,
- * redirect them to the last page/pageCount
- */
- if (pageCount > 0 && currentPage > pageCount) {
- const destination = resolvedUrl.replace(`page=${currentPage}`, `page=${pageCount}`);
- const redirect: GetServerSidePropsResult = {
- redirect: {
- destination,
- permanent: false,
- },
- };
- return redirect as never; // Exclude this return type from the return signature
- }
-
- return {
- props: {
- categoryFilters,
- flavourFilters,
- styleFilters,
- variants,
- itemCount,
- pageCount,
- pageSize,
- currentPage,
- },
- };
-}) satisfies GetServerSideProps;
-
export default ProductsPage;
diff --git a/packages/nextjs/app/migration/sorting.ts b/packages/nextjs/app/migration/sorting.ts
new file mode 100644
index 00000000..f9bcdbeb
--- /dev/null
+++ b/packages/nextjs/app/migration/sorting.ts
@@ -0,0 +1,52 @@
+// See: ./getOrderingFromQuery
+
+export enum SortType {
+ Default = "DEFAULT", // no sort
+ Natural = "NATURAL", // sorts alphanumerical
+}
+
+export type SortOption = {
+ value: string;
+ label: string;
+ type: SortType;
+ ordering: `${string} ${"asc" | "desc"}`;
+};
+
+export const SORT_QUERY_PARAM = "sort";
+
+export const PAGE_QUERY_PARAM = "page";
+
+export const SORT_OPTIONS: Record = {
+ default: {
+ value: "default",
+ label: "Default",
+ type: SortType.Default,
+ ordering: "_id asc", // Default query sorting
+ },
+ "a-z": {
+ value: "a-z",
+ label: "A - Z",
+ type: SortType.Natural,
+ ordering: "name asc",
+ },
+ "z-a": {
+ value: "z-a",
+ label: "Z - A",
+ type: SortType.Natural,
+ ordering: "name desc",
+ },
+ lowest: {
+ value: "lowest",
+ label: "Lowest Price",
+ type: SortType.Natural,
+ ordering: "price asc",
+ },
+ highest: {
+ value: "highest",
+ label: "Highest Price",
+ type: SortType.Natural,
+ ordering: "price desc",
+ },
+};
+
+export const SORT_OPTIONS_ARRAY = Object.values(SORT_OPTIONS);
diff --git a/packages/nextjs/app/page.tsx b/packages/nextjs/app/page.tsx
new file mode 100644
index 00000000..667aed92
--- /dev/null
+++ b/packages/nextjs/app/page.tsx
@@ -0,0 +1,28 @@
+import { Metadata } from "next";
+import HomePage from "./migration/home-page";
+import { getAllCategories } from "utils/getAllCategoriesQuery";
+import { getRecommendations } from "utils/getRecommendationsQuery";
+
+export const metadata: Metadata = {
+ title: "Home – Formidable Boulangerie",
+ description: "Formidable Boulangerie home page. A showcase of Next.js, Sanity CMS, and Fastly CDN.",
+ openGraph: {
+ title: "Home – Formidable Boulangerie",
+ description: "Formidable Boulangerie home page. A showcase of Next.js, Sanity CMS, and Fastly CDN.",
+ },
+};
+
+async function getData() {
+ const categories = await getAllCategories();
+ const products = await getRecommendations();
+
+ return {
+ products,
+ categories,
+ };
+}
+
+export default async function Page() {
+ const data = await getData();
+ return ;
+}
diff --git a/packages/nextjs/app/products/[slug]/page.tsx b/packages/nextjs/app/products/[slug]/page.tsx
new file mode 100644
index 00000000..1a0a28ce
--- /dev/null
+++ b/packages/nextjs/app/products/[slug]/page.tsx
@@ -0,0 +1,20 @@
+import ProductsPage from "app/migration/products/[slug]";
+import { getProductBySlug } from "utils/getProductBySlug";
+import { getRecommendations } from "utils/getRecommendationsQuery";
+import { isSlug } from "utils/isSlug";
+
+const getData = async (slug: string) => {
+ const products = await getProductBySlug(isSlug(slug) ? slug : "");
+ const recommendations = await getRecommendations();
+
+ return {
+ product: products[0],
+ recommendations,
+ };
+};
+
+export default async function Page({ params }: { params: { slug: string } }) {
+ const data = await getData(params.slug);
+
+ return ;
+}
diff --git a/packages/nextjs/app/products/page.tsx b/packages/nextjs/app/products/page.tsx
new file mode 100644
index 00000000..fdb8ceea
--- /dev/null
+++ b/packages/nextjs/app/products/page.tsx
@@ -0,0 +1,58 @@
+import { redirect } from "next/navigation";
+import { getOrderingFromQuery } from "../migration/getOrderingFromQuery";
+import { getAllFilteredVariants } from "utils/getFilteredPaginatedQuery";
+import { getCategoryFilters, getFlavourFilters, getStyleFilters } from "utils/getFilters";
+import { getFiltersFromQuery } from "utils/getFiltersFromQuery";
+import { getPaginationFromQuery } from "utils/getPaginationFromQuery";
+import ProductsPage from "app/migration/products";
+
+// See: https://nextjs.org/docs/app/api-reference/file-conventions/page
+type RouteSearchParams = { [key: string]: string | string[] | undefined };
+
+const getData = async ({ searchParams }: { searchParams: RouteSearchParams }) => {
+ // Sort/ordering.
+ const order = getOrderingFromQuery(searchParams);
+
+ // Fetch size filters from sanity
+ const categoryFilters = await getCategoryFilters();
+ const flavourFilters = await getFlavourFilters();
+ const styleFilters = await getStyleFilters();
+
+ // Filters.
+ const filters = getFiltersFromQuery(searchParams, { flavourFilters, styleFilters, categoryFilters });
+ // Pagination data.
+ const pagination = getPaginationFromQuery(searchParams);
+ const result = await getAllFilteredVariants(filters, order, pagination);
+
+ const { variants, itemCount } = result;
+ const { currentPage, pageSize } = pagination;
+ const pageCount = Math.ceil(itemCount / pageSize);
+
+ /**
+ * Scenario: If user is on the third page and then enables
+ * a filter that only returns two pages worth of products,
+ * redirect them to the last page/pageCount
+ */
+
+ return {
+ categoryFilters,
+ flavourFilters,
+ styleFilters,
+ variants,
+ itemCount,
+ pageCount,
+ pageSize,
+ currentPage,
+ };
+};
+
+export default async function Page({ searchParams }: { searchParams: RouteSearchParams }) {
+ const data = await getData({ searchParams });
+
+ if (data.pageCount > 0 && data.currentPage > data.pageCount) {
+ const newParams = new URLSearchParams({ ...searchParams, page: data.pageCount.toString() });
+ return redirect(`/products?${newParams.toString()}`);
+ }
+
+ return ;
+}
diff --git a/packages/nextjs/app/template.tsx b/packages/nextjs/app/template.tsx
new file mode 100644
index 00000000..dbf0fa0b
--- /dev/null
+++ b/packages/nextjs/app/template.tsx
@@ -0,0 +1,8 @@
+"use client";
+
+import { FadeInOut } from "./ui/shared-ui";
+
+// TODO: exit animations currently do not work in next app router
+export default function Template({ children }: { children: React.ReactNode }) {
+ return {children} ;
+}
diff --git a/packages/nextjs/app/ui/framer.tsx b/packages/nextjs/app/ui/framer.tsx
new file mode 100644
index 00000000..ead38288
--- /dev/null
+++ b/packages/nextjs/app/ui/framer.tsx
@@ -0,0 +1,3 @@
+"use client";
+
+export { MotionConfig, AnimatePresence } from "framer-motion";
diff --git a/packages/nextjs/app/ui/shared-ui.ts b/packages/nextjs/app/ui/shared-ui.ts
new file mode 100644
index 00000000..325b7d14
--- /dev/null
+++ b/packages/nextjs/app/ui/shared-ui.ts
@@ -0,0 +1,5 @@
+"use client";
+
+// TODO: separate imports so this isn't needed
+// for server-compatible components like Footer
+export * from "shared-ui";
diff --git a/packages/nextjs/components/Breadcrumbs.tsx b/packages/nextjs/components/Breadcrumbs.tsx
index 20db3228..d7f666dd 100644
--- a/packages/nextjs/components/Breadcrumbs.tsx
+++ b/packages/nextjs/components/Breadcrumbs.tsx
@@ -1,8 +1,8 @@
import React from "react";
-import { useRouter } from "next/router";
import { MdOutlineHome } from "react-icons/md";
import { BreadcrumbItem, BreadcrumbsContainer, capitalizeWords } from "shared-ui";
import Link from "next/link";
+import { usePathname, useSearchParams } from "next/navigation";
type LinkElement = {
title: string;
@@ -10,9 +10,9 @@ type LinkElement = {
};
export const Breadcrumbs = () => {
- const { query, asPath } = useRouter();
- // Remove query string.
- const urlPath = asPath.split("?")[0];
+ const searchParams = useSearchParams();
+ const query = searchParams ? Object.fromEntries(searchParams?.entries()) : {};
+ const urlPath = usePathname() ?? "";
// Normal route.
let pathElements = urlPath.split("/");
@@ -48,7 +48,7 @@ export const Breadcrumbs = () => {
return (
-
+
Home
{elements.map(({ title, href }, index) => (
diff --git a/packages/nextjs/components/CartContext.tsx b/packages/nextjs/components/CartContext.tsx
index 1a06fd89..47216339 100644
--- a/packages/nextjs/components/CartContext.tsx
+++ b/packages/nextjs/components/CartContext.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import * as React from "react";
import { q } from "groqd";
import { CartItem, CartUpdate, CartProvider as SharedCartProvider } from "shared-ui";
diff --git a/packages/nextjs/components/Header/Header.tsx b/packages/nextjs/components/Header/Header.tsx
index 829082a2..49a03b71 100644
--- a/packages/nextjs/components/Header/Header.tsx
+++ b/packages/nextjs/components/Header/Header.tsx
@@ -1,8 +1,10 @@
+"use client"; // TODO: move down the tree
+
import * as React from "react";
import Link from "next/link";
import { Button, useCart, Header as BaseHeader, useMobileNav } from "shared-ui";
import { Search } from "components/Search";
-import { useRouter } from "next/router";
+import { usePathname } from "next/navigation";
import { NAV_ITEMS } from "shared-ui";
import { DesktopNavItem } from "shared-ui";
import { Cart } from "shared-ui";
@@ -12,7 +14,7 @@ import { MobileNavMenu } from "shared-ui";
export const Header = () => {
const { toggleCartOpen } = useCart();
- const { pathname } = useRouter();
+ const pathname = usePathname() || "";
const { onMobileNavClose } = useMobileNav();
return (
diff --git a/packages/nextjs/components/Pagination.tsx b/packages/nextjs/components/Pagination.tsx
index 58331d79..62377f0d 100644
--- a/packages/nextjs/components/Pagination.tsx
+++ b/packages/nextjs/components/Pagination.tsx
@@ -2,8 +2,7 @@ import * as React from "react";
import { Pagination as BasePagination } from "shared-ui";
import Link from "next/link";
import classNames from "classnames";
-import { useRouter } from "next/router";
-import { stringify } from "querystring";
+import { usePathname, useSearchParams } from "next/navigation";
type PaginationProps = {
pageCount: number;
@@ -12,7 +11,9 @@ type PaginationProps = {
};
export const Pagination = ({ onPageChange, pageCount = 1, currentPage = 1 }: PaginationProps) => {
- const router = useRouter();
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+
const handlePageChanged = (e: React.MouseEvent, page: number) => {
if (onPageChange) {
e.preventDefault();
@@ -25,8 +26,8 @@ export const Pagination = ({ onPageChange, pageCount = 1, currentPage = 1 }: Pag
pageCount={pageCount}
NextPreviousLink={Link}
currentPage={currentPage}
- currentHref={router.pathname}
- search={stringify(router.query)}
+ currentHref={pathname ?? ""}
+ search={searchParams?.toString() ?? ""}
renderPaginationLink={({ page, href }) => (
= ({ group }) => {
const { query, add, remove } = useRouterQueryParams();
- const queryValue = query[groupValue];
- const queryValueIsString = typeof queryValue === "string" || queryValue instanceof String;
+ const queryValue = query?.get(groupValue);
const handleChange = (e: ChangeEvent) => {
const { checked, value: optionValue } = e.target;
@@ -33,8 +32,8 @@ export const FilterGroup: React.FC = ({ group }) => {
{options.map(({ value: optionValue, label: optionLabel }) => {
const isChecked =
!!queryValue && // Value exists
- ((queryValueIsString && queryValue === optionValue) || // Single value matches option
- (!queryValueIsString && queryValue.includes(optionValue))); // Multiple values includes option
+ (queryValue === optionValue || // Single value matches option
+ queryValue.includes(optionValue)); // Multiple values includes option
return (
diff --git a/packages/nextjs/components/ProductPage/ProductVariantSelector.tsx b/packages/nextjs/components/ProductPage/ProductVariantSelector.tsx
index d8064bc1..ac2273a4 100644
--- a/packages/nextjs/components/ProductPage/ProductVariantSelector.tsx
+++ b/packages/nextjs/components/ProductPage/ProductVariantSelector.tsx
@@ -1,11 +1,11 @@
import * as React from "react";
import { useMemo } from "react";
import { H6, Select } from "shared-ui";
-import { Variant } from "utils/groqTypes/ProductList";
+import { ProductDetailVariants } from "utils/groqTypes/ProductDetail";
interface Props {
- variants: Variant[];
- selectedVariant?: Variant;
+ variants: ProductDetailVariants;
+ selectedVariant?: ProductDetailVariants[number];
onVariantChange: (slug?: string) => void;
}
@@ -31,6 +31,7 @@ export const ProductVariantSelector = ({ variants, selectedVariant, onVariantCha
) : (
= (props) => {
const { replace, clear, query } = useRouterQueryParams();
- return ;
+ const searchParams = new URLSearchParams(query ?? "");
+
+ return ;
};
diff --git a/packages/nextjs/middleware.ts b/packages/nextjs/middleware.ts
new file mode 100644
index 00000000..c5ae3137
--- /dev/null
+++ b/packages/nextjs/middleware.ts
@@ -0,0 +1,44 @@
+import { NextRequest, NextResponse } from "next/server";
+import { SanityType } from "utils/consts";
+import { getProductBySlug } from "utils/getProductBySlug";
+
+type Product = Awaited>[number];
+
+export const config = {
+ matcher: ["/", "/categories", "/products", "/products/:slug*"],
+};
+
+export async function middleware(request: NextRequest) {
+ const responseHeaders = new Headers();
+ responseHeaders.set("Cache-Control", "public, max-age=0");
+ responseHeaders.set("Surrogate-Control", "max-age=604800, stale-while-revalidate=120000, stale-if-error=600000");
+
+ if (request.nextUrl.pathname === "/" || request.nextUrl.pathname === "/categories") {
+ const keys = [SanityType.Category, SanityType.CategoryImage];
+ responseHeaders.set("Surrogate-Key", keys.join(" "));
+ }
+
+ if (request.nextUrl.pathname === "/products") {
+ const keys = [SanityType.Product, SanityType.Style, SanityType.Flavour, SanityType.Variant];
+ responseHeaders.set("Surrogate-Key", keys.join(" "));
+ } else if (request.nextUrl.pathname.startsWith("/products")) {
+ const [, , slug] = request.nextUrl.pathname.split("/");
+ const products = await getProductBySlug(slug);
+ const variantSlugs = (products[0]?.variants?.map((v: Product["variants"][number]) => v?.slug) || []).filter(
+ Boolean
+ );
+
+ const cacheKeys = [
+ `${SanityType.Product}_${slug}`,
+ "testing",
+ ...variantSlugs.map((s) => `${SanityType.Variant}_${s}`),
+ ];
+ responseHeaders.set("Surrogate-Key2", cacheKeys.join(" "));
+ }
+
+ const response = NextResponse.next({
+ headers: responseHeaders,
+ });
+
+ return response;
+}
diff --git a/packages/nextjs/next.config.js b/packages/nextjs/next.config.js
index d9f2edbe..04543589 100644
--- a/packages/nextjs/next.config.js
+++ b/packages/nextjs/next.config.js
@@ -8,9 +8,6 @@ const nextConfig = {
eslint: {
ignoreDuringBuilds: true,
},
- experimental: {
- appDir: true,
- },
};
module.exports = nextConfig;
diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json
index 3d43fccb..e7488df3 100644
--- a/packages/nextjs/package.json
+++ b/packages/nextjs/package.json
@@ -36,7 +36,7 @@
"groqd-playground": "^0.0.12",
"jsdom": "^20.0.0",
"lodash.debounce": "^4.0.8",
- "next": "^13.4.0",
+ "next": "^14.0.3",
"next-sanity": "^4.2.0",
"node-fetch": "^3.2.10",
"nuka-carousel": "^5.3.0",
@@ -45,8 +45,8 @@
"react-icons": "^4.4.0",
"react-is": "^18.2.0",
"sanity": "^3.9.1",
- "styled-components": "^5.2.0",
- "shared-ui": "workspace:*"
+ "shared-ui": "workspace:*",
+ "styled-components": "^5.2.0"
},
"devDependencies": {
"@portabletext/types": "^2.0.0",
diff --git a/packages/nextjs/pages/_app.tsx b/packages/nextjs/pages/_app.tsx
deleted file mode 100644
index 7111e5f9..00000000
--- a/packages/nextjs/pages/_app.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import type { AppProps } from "next/app";
-import * as React from "react";
-import Head from "next/head";
-import { AnimatePresence, MotionConfig } from "framer-motion";
-
-import { FadeInOut } from "shared-ui";
-import { CartProvider } from "components/CartContext";
-import { Layout } from "components/Layout";
-import "styles/global.css";
-
-(async () => {
- if (process.env.NEXT_PUBLIC_API_MOCKING === "enabled") {
- await import("mocks/msw/msw-enable");
- }
-})();
-
-function MyApp({ Component, pageProps, router }: AppProps) {
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-}
-
-export default MyApp;
diff --git a/packages/nextjs/pages/_document.tsx b/packages/nextjs/pages/_document.tsx
deleted file mode 100644
index d828bb32..00000000
--- a/packages/nextjs/pages/_document.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Html, Head, Main, NextScript } from "next/document";
-
-export default function Document() {
- return (
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/packages/nextjs/styles/global.css b/packages/nextjs/styles/global.css
deleted file mode 100644
index af546bf1..00000000
--- a/packages/nextjs/styles/global.css
+++ /dev/null
@@ -1,42 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-body {
- @apply bg-secondary;
-}
-
-
-@font-face {
- font-family: "JeanLuc";
- src: url('../assets/fonts/jeanluc/jeanlucweb-bold.woff');
- font-weight: bold;
- font-style: normal;
-}
-
-@font-face {
- font-family: "JeanLuc";
- src: url('../assets/fonts/jeanluc/jeanlucweb-thin.woff');
- font-weight: auto;
- font-style: normal;
-}
-
-@font-face {
- font-family: "JetBrains Mono";
- src: url('../assets/fonts/jetbrainsmono/JetBrainsMono-Bold.woff2');
- font-weight: bold;
- font-style: normal;
-}
-
-@font-face {
- font-family: "JetBrains Mono";
- src: url('../assets/fonts/jetbrainsmono/JetBrainsMono-Regular.woff2');
- font-weight: auto;
- font-style: normal;
-}
-
-code {
- @apply rounded-md bg-black/5 px-1.5 py-0.5;
- font-family: 'JetBrains Mono', monospace;
- font-size: 0.9rem;
-}
\ No newline at end of file
diff --git a/packages/nextjs/tailwind.config.js b/packages/nextjs/tailwind.config.js
index 145d9d32..6dc93414 100644
--- a/packages/nextjs/tailwind.config.js
+++ b/packages/nextjs/tailwind.config.js
@@ -3,6 +3,7 @@ const defaultTheme = require("tailwindcss/defaultTheme");
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
"./pages/**/*.{js,jsx,ts,tsx}",
"./components/**/*.{js,jsx,ts,tsx}",
"./views/**/*.{js,jsx,ts,tsx}",
diff --git a/packages/nextjs/utils/getFiltersCount.ts b/packages/nextjs/utils/getFiltersCount.ts
index 794194ae..f1b99aa7 100644
--- a/packages/nextjs/utils/getFiltersCount.ts
+++ b/packages/nextjs/utils/getFiltersCount.ts
@@ -1,13 +1,12 @@
-import { useRouter } from "next/router";
-import { ParsedUrlQuery } from "querystring";
+import { ReadonlyURLSearchParams, useSearchParams } from "next/navigation";
import type { FilterGroup } from "./filters";
import { getFilterGroups } from "./filters";
-export const getFiltersCount = (query: ParsedUrlQuery) => {
+export const getFiltersCount = (query: ReadonlyURLSearchParams | null) => {
const filters = getFilterGroups();
const total = filters.reduce((acc: number, { label, value }: FilterGroup) => {
- const selectedFilters = query[value];
+ const selectedFilters = query?.get(value);
const elements = selectedFilters?.length ?? 0;
// if a single element is selected, type would be a string instead of an array.
@@ -20,7 +19,6 @@ export const getFiltersCount = (query: ParsedUrlQuery) => {
};
export const useGetFiltersCount = () => {
- const router = useRouter();
-
- return getFiltersCount(router.query);
+ const query = useSearchParams();
+ return getFiltersCount(query);
};
diff --git a/packages/nextjs/utils/getProductBySlug.ts b/packages/nextjs/utils/getProductBySlug.ts
index af9f4386..a217fde8 100644
--- a/packages/nextjs/utils/getProductBySlug.ts
+++ b/packages/nextjs/utils/getProductBySlug.ts
@@ -1,40 +1,38 @@
import { q, sanityImage } from "groqd";
import { runQuery } from "./sanityClient";
-export const getProductBySlug = (slug = "") =>
- runQuery(
- q("*")
- .filterByType("product")
- .filter("slug.current == $slug")
- .grab({
- _id: q.string(),
- name: q.string(),
- categories: q("categories").filter().deref().grab$({
- name: q.string(),
- }),
- slug: q.slug("slug"),
- variants: q("variants")
- .filter()
- .deref()
- .grab$({
- _id: q.string(),
- name: q.string(),
- description: q.contentBlocks(),
- msrp: q.number(),
- price: q.number(),
- slug: q.slug("slug"),
- images: sanityImage("images", {
- isList: true,
- }),
- style: q("style")
- .filter()
- .deref()
- .grab$({
- _id: q.string(),
- name: q.string(),
- })
- .nullable(),
- }),
+export const productBySlugSelection = {
+ _id: q.string(),
+ name: q.string(),
+ categories: q("categories").filter().deref().grab$({
+ name: q.string(),
+ }),
+ slug: q.slug("slug"),
+ variants: q("variants")
+ .filter()
+ .deref()
+ .grab$({
+ _id: q.string(),
+ name: q.string(),
+ description: q.contentBlocks(),
+ msrp: q.number(),
+ price: q.number(),
+ slug: q.slug("slug"),
+ images: sanityImage("images", {
+ isList: true,
+ withCrop: true,
+ withHotspot: true,
}),
- { slug }
- );
+ style: q("style")
+ .filter()
+ .deref()
+ .grab$({
+ _id: q.string(),
+ name: q.string(),
+ })
+ .nullable(),
+ }),
+};
+
+export const getProductBySlug = (slug = "") =>
+ runQuery(q("*").filterByType("product").filter("slug.current == $slug").grab(productBySlugSelection), { slug });
diff --git a/packages/nextjs/utils/groqTypes/ProductDetail.ts b/packages/nextjs/utils/groqTypes/ProductDetail.ts
new file mode 100644
index 00000000..f1cace67
--- /dev/null
+++ b/packages/nextjs/utils/groqTypes/ProductDetail.ts
@@ -0,0 +1,7 @@
+import { TypeFromSelection } from "groqd";
+import { productBySlugSelection } from "utils/getProductBySlug";
+import { NullableArrayType } from "./ProductList";
+
+export type ProductDetail = TypeFromSelection;
+export type ProductDetailVariants = ProductDetail["variants"];
+export type Style = NullableArrayType[number];
diff --git a/packages/nextjs/utils/groqTypes/ProductList.ts b/packages/nextjs/utils/groqTypes/ProductList.ts
index 654fe7d3..09469f06 100644
--- a/packages/nextjs/utils/groqTypes/ProductList.ts
+++ b/packages/nextjs/utils/groqTypes/ProductList.ts
@@ -32,7 +32,7 @@ export interface CategoryFilterItem {
slug: string;
}
-type NullableArrayType = Exclude;
+export type NullableArrayType = Exclude;
export type Flavour = NullableArrayType[number];
export type Style = NullableArrayType[number];
diff --git a/packages/nextjs/utils/setCachingHeaders.ts b/packages/nextjs/utils/setCachingHeaders.ts
deleted file mode 100644
index c9441a2d..00000000
--- a/packages/nextjs/utils/setCachingHeaders.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { ServerResponse } from "http";
-
-/**
- * sets appropriate caching response header, along with an optional set of surrogate keys
- * note: when running `next dev`, these headers are overwritten to prevent local caching
- * @param res - ServerResponse object
- * @param keys - array of keys to add to the Surrogate-Key response header
- */
-export const setCachingHeaders = (res: ServerResponse, keys?: string[]): void => {
- res.setHeader("Cache-Control", "public, max-age=0");
- res.setHeader("Surrogate-Control", "max-age=604800, stale-while-revalidate=120000, stale-if-error=600000");
- if (keys && keys.length > 0) {
- res.setHeader("Surrogate-Key", keys.join(" "));
- }
-};
diff --git a/packages/nextjs/utils/useRouterQueryParams.ts b/packages/nextjs/utils/useRouterQueryParams.ts
index 524630a1..de637e79 100644
--- a/packages/nextjs/utils/useRouterQueryParams.ts
+++ b/packages/nextjs/utils/useRouterQueryParams.ts
@@ -1,51 +1,36 @@
-import { useRouter } from "next/router";
+import { usePathname, useRouter, useSearchParams } from "next/navigation";
export const useRouterQueryParams = () => {
const router = useRouter();
+ const pathname = usePathname();
+ const query = useSearchParams();
+ const current = new URLSearchParams(query ?? "");
const add = (key: string, value: string) => {
- const query = { ...router.query };
- const currentValue = query[key];
- if (currentValue) {
- if (typeof currentValue === "string" || currentValue instanceof String) {
- // Single existing, create and append to array
- query[key] = [currentValue as string, value];
- } else {
- // Multiple options, append to array
- (query[key] as string[]).push(value);
- }
- } else {
- // No existing options, sdd single option
- query[key] = value;
- }
- router.replace({ query });
+ current.append(key, value);
+ router.replace(`${pathname}?${current.toString()}`);
};
const replace = (args: Record) => {
- const query = { ...router.query, ...args };
- router.replace({ query });
+ const currentQuery = Object.fromEntries(current);
+ const updatedEntries = {
+ ...currentQuery,
+ ...args,
+ };
+ const newQuery = new URLSearchParams(updatedEntries).toString();
+
+ router.replace(`${pathname}?${newQuery}`);
};
const clear = (key: string) => {
- const query = { ...router.query };
- delete query[key];
- router.replace({ query });
+ current.delete(key);
+ router.replace(`${pathname}?${current.toString()}`);
};
const remove = (key: string, value: string) => {
- const query = { ...router.query };
- const currentValue = query[key];
- if (currentValue) {
- if (typeof currentValue === "string" || currentValue instanceof String) {
- // Single option, remove group and option
- delete query[key];
- } else {
- // Multiple options, only remove option
- query[key] = (query[key] as string[]).filter((curr) => curr !== value);
- }
- }
- router.replace({ query });
+ current.delete(key, value);
+ router.replace(`${pathname}?${current.toString()}`);
};
- return { add, replace, clear, remove, query: router.query };
+ return { add, replace, clear, remove, query };
};
diff --git a/packages/shared-ui/components/ProductSort/ProductSort.tsx b/packages/shared-ui/components/ProductSort/ProductSort.tsx
index 847d485b..17210e09 100644
--- a/packages/shared-ui/components/ProductSort/ProductSort.tsx
+++ b/packages/shared-ui/components/ProductSort/ProductSort.tsx
@@ -52,6 +52,7 @@ export const ProductSort: React.FC = ({
void;
@@ -18,11 +19,12 @@ export interface Props {
const itemToString = (item: Option | null) => (item ? item.title : "");
-export function Select({ label, placeholder, options, className, selectedItem, onChange }: Props) {
+export function Select({ label, placeholder, options, className, selectedItem, onChange, id }: Props) {
const { isOpen, getToggleButtonProps, getLabelProps, getMenuProps, highlightedIndex, getItemProps } = useSelect({
items: options,
itemToString,
selectedItem,
+ id,
onSelectedItemChange({ selectedItem }) {
onChange && onChange(selectedItem);
},
diff --git a/packages/shared-ui/components/cart/Cart.tsx b/packages/shared-ui/components/cart/Cart.tsx
index cff31d03..30402f76 100644
--- a/packages/shared-ui/components/cart/Cart.tsx
+++ b/packages/shared-ui/components/cart/Cart.tsx
@@ -47,7 +47,7 @@ export const Cart = ({ onMobileNavClose, children }: React.PropsWithChildren
- Cart
+ Cart
{isLoading ? (
diff --git a/packages/shared-ui/components/cart/stories/Cart.stories.tsx b/packages/shared-ui/components/cart/stories/Cart.stories.tsx
index 435490f6..f43ed803 100644
--- a/packages/shared-ui/components/cart/stories/Cart.stories.tsx
+++ b/packages/shared-ui/components/cart/stories/Cart.stories.tsx
@@ -124,6 +124,7 @@ export const API: StoryObj = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step("add to cart", async () => {
+ await waitFor(() => expect(canvas.getByTestId("cart")).toHaveTextContent("Cart1"));
await userEvent.click(canvas.getByRole("button", { name: /product1/i }));
await userEvent.click(canvas.getByRole("button", { name: /product2/i }));
await waitFor(() => expect(canvas.getByTestId("cart")).toHaveTextContent("Cart3"));
diff --git a/packages/tests-e2e/cypress/support/commands/getServerSideProps.ts b/packages/tests-e2e/cypress/support/commands/getServerSideProps.ts
deleted file mode 100644
index 466039e2..00000000
--- a/packages/tests-e2e/cypress/support/commands/getServerSideProps.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import type * as home from " ../../../../nextjs/pages/index";
-import type * as categories from " ../../../../nextjs/pages/categories";
-import type * as products from " ../../../../nextjs/pages/products/index";
-import type * as product from " ../../../../nextjs/pages/products/[slug]";
-
-type AsyncReturnType any> = UnwrapPromise>;
-type UnwrapPromise = TPromiseMaybe extends Promise ? TValue : TPromiseMaybe;
-
-type PageProps = {
- "/home": AsyncReturnType["props"];
- "/categories": AsyncReturnType["props"];
- "/products": AsyncReturnType["props"];
- "/products/[slug]": AsyncReturnType["props"];
-};
-const getServerSidePropsForPage = {
- "/home": (props) => props,
- "/categories": (props) => props,
- "/products": (props) => props,
- "/products/[slug]": (props) => props,
-} satisfies { [P in keyof PageProps]: (props: PageProps[P]) => unknown };
-
-export type PageDataTypes = {
- [P in keyof typeof getServerSidePropsForPage]: ReturnType<(typeof getServerSidePropsForPage)[P]>;
-};
-
-declare global {
- // eslint-disable-next-line @typescript-eslint/no-namespace
- namespace Cypress {
- export interface Chainable {
- /**
- * Retrieves the page's server-side props from __NEXT_DATA__.
- */
- getServerSideProps(page: TPage): Chainable;
- }
- }
-}
-
-Cypress.Commands.add(
- "getServerSideProps",
- (
- // We only use this parameter for strong types:
- page: TPage // eslint-disable-line @typescript-eslint/no-unused-vars
- ): Cypress.Chainable => {
- return cy.window().then((win) => {
- const props = win.__NEXT_DATA__.props.pageProps as any;
- const mapped = getServerSidePropsForPage[page](props as any) as PageDataTypes[TPage];
- return mapped;
- });
- }
-);
diff --git a/packages/tests-e2e/cypress/support/commands/index.ts b/packages/tests-e2e/cypress/support/commands/index.ts
index 6a0003f1..e76eb340 100644
--- a/packages/tests-e2e/cypress/support/commands/index.ts
+++ b/packages/tests-e2e/cypress/support/commands/index.ts
@@ -1,6 +1,5 @@
import "@testing-library/cypress/add-commands";
-import "./getServerSideProps";
import "./nextClosest";
import "./setMockData";
diff --git a/packages/tests-e2e/e2e-tests/categories.cy.ts b/packages/tests-e2e/e2e-tests/categories.cy.ts
index 4e1de982..e2af74a6 100644
--- a/packages/tests-e2e/e2e-tests/categories.cy.ts
+++ b/packages/tests-e2e/e2e-tests/categories.cy.ts
@@ -1,5 +1,4 @@
import { mock } from "mocks/factory";
-import { Category } from "utils/groqTypes/ProductList";
import { mockOnly } from "../utils/real-or-mock";
describe(`when I visit the "Categories" page`, () => {
@@ -20,14 +19,11 @@ describe(`when I visit the "Categories" page`, () => {
});
it("I see at least 3 Categories on the page", () => {
- cy.getServerSideProps("/categories").then((data) => {
- cy.get("main").within(() => {
- const categories = data.categories;
- expect(categories).to.have.length.at.least(3);
- categories.forEach((cat: Category) => {
- expect(cat.name).to.not.be.empty;
- cy.findAllByText(cat.name!).should("exist");
- });
+ cy.get("main").within(() => {
+ const categories = ["Delectable Donuts", "Palatable Pastries", "Love Loaves"];
+
+ categories.forEach((category) => {
+ cy.findAllByText(category).should("exist");
});
});
});
diff --git a/packages/tests-e2e/e2e-tests/home.cy.ts b/packages/tests-e2e/e2e-tests/home.cy.ts
index 958ed6a7..d46b3d82 100644
--- a/packages/tests-e2e/e2e-tests/home.cy.ts
+++ b/packages/tests-e2e/e2e-tests/home.cy.ts
@@ -13,13 +13,10 @@ describe("when I visit the home page", () => {
.nextClosest("section")
.within(() => {
cy.findByText("Show all breads").should("exist");
- cy.getServerSideProps("/home").then((props) => {
- const bestSellers = props.data.products;
- expect(bestSellers).to.have.length(3);
- for (const product of bestSellers) {
- cy.findByText(product.name!).should("exist");
- }
- });
+ const bestSellers = ["Kouign Amann", "Sourdough Loaf", "Pain au Chocolat"];
+ for (const product of bestSellers) {
+ cy.findByText(product).should("exist");
+ }
});
});
it('I see the "Top Categories" section with 3 categories', () => {
@@ -27,13 +24,11 @@ describe("when I visit the home page", () => {
.should("exist")
.nextClosest("section")
.within(() => {
- cy.getServerSideProps("/home").then((props) => {
- const categories = props.data.categories.slice(0, 3);
- cy.get('[data-testid="featured-list-item"]').should("have.length", 3);
- for (const category of categories) {
- cy.findByText(category.name!).should("exist");
- }
- });
+ const categories = ["Delectable Donuts", "Palatable Pastries", "Love Loaves"];
+ cy.get('[data-testid="featured-list-item"]').should("have.length", 3);
+ for (const category of categories) {
+ cy.findByText(category).should("exist");
+ }
});
});
});
diff --git a/packages/tests-e2e/e2e-tests/product.cy.ts b/packages/tests-e2e/e2e-tests/product.cy.ts
index d787cd1d..0d587d13 100644
--- a/packages/tests-e2e/e2e-tests/product.cy.ts
+++ b/packages/tests-e2e/e2e-tests/product.cy.ts
@@ -5,10 +5,7 @@ describe("when I visit the Product Details Page", () => {
realOnly.before(() => {
// Find a product to test:
cy.visit("/products");
- cy.getServerSideProps("/products").then((props) => {
- const product = props.variants[0];
- cy.visit(`/products/${product.productSlug}`);
- });
+ cy.visit("/products/sourdough-loaf?variant=seeded-sourdough-loaf");
});
mockOnly.before(() => {
@@ -18,23 +15,16 @@ describe("when I visit the Product Details Page", () => {
});
it(`I see the product's title`, () => {
- cy.getServerSideProps("/products/[slug]").then((props) => {
- const product = props.data.products[0];
- cy.findAllByText(product.name!).should("exist");
- });
+ cy.findAllByText("Sourdough Loaf").should("exist");
});
+
it("I see the item's price", () => {
- cy.getServerSideProps("/products/[slug]").then((props) => {
- const product = props.data.products[0];
- const variant = product.variants![0]!;
- cy.findAllByText(/\$\d+\.\d\d/)
- .should("exist")
- .then((price) => {
- const renderedPrice = price.first().text();
- const expectedPrice = variant.price!;
- expect(renderedPrice).to.equal(`$${expectedPrice.toFixed(2)}`);
- });
- });
+ cy.findAllByText(/\$/i)
+ .should("exist")
+ .then((price) => {
+ const renderedPrice = price.first().text();
+ expect(renderedPrice).to.equal(`$7.98`);
+ });
});
["Type", "Style", "Quantity"].forEach((option) => {
diff --git a/packages/tests-e2e/e2e-tests/products.cy.ts b/packages/tests-e2e/e2e-tests/products.cy.ts
index 9632db35..985558fd 100644
--- a/packages/tests-e2e/e2e-tests/products.cy.ts
+++ b/packages/tests-e2e/e2e-tests/products.cy.ts
@@ -1,5 +1,4 @@
import { generateMockData } from "mocks/msw/db/mock-data";
-import type { PageDataTypes } from "../cypress/support/commands/getServerSideProps";
import { mockOnly } from "../utils/real-or-mock";
describe("when I visit the products page", () => {
@@ -44,29 +43,16 @@ describe("when I visit the products page", () => {
});
describe("on the default page", () => {
- let pageProps: PageDataTypes["/products"];
-
- before(async () => {
- cy.getServerSideProps("/products").then((props) => {
- pageProps = props;
- });
- });
-
- const EXPECTED_ITEMS_PER_PAGE = 6;
- const EXPECTED_ITEMS_MINIMUM = 10;
-
it("I see 6 products", () => {
- expect(pageProps.variants.length).to.equal(
- EXPECTED_ITEMS_PER_PAGE,
- `there should be ${EXPECTED_ITEMS_PER_PAGE} items on this page`
- );
- expect(pageProps.itemCount).to.gte(
- EXPECTED_ITEMS_MINIMUM,
- `there should be at least ${EXPECTED_ITEMS_MINIMUM} items`
- );
-
- pageProps.variants.forEach((variant) => {
- cy.findByText(variant.name).should("exist");
+ [
+ "Seeded Sourdough Loaf",
+ "Pain au Chocolat",
+ "Sourdough Loaf",
+ "Chocolate Croissant",
+ "Whole Grain Potato Rosemary",
+ "Lemon Tart",
+ ].forEach((variant) => {
+ cy.findByText(variant).should("exist");
});
});
@@ -74,7 +60,7 @@ describe("when I visit the products page", () => {
cy.findByText("Previous").should("exist");
cy.findByText("Next").should("exist");
- const pageCount = pageProps.pageCount;
+ const pageCount = 4;
for (let i = 0; i < pageCount; i++) {
cy.findByText(`${i + 1}`).should("exist");
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f0a8e751..ed69eca0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -130,11 +130,11 @@ importers:
specifier: ^4.0.8
version: 4.0.8
next:
- specifier: ^13.4.0
- version: 13.5.6(react-dom@18.2.0)(react@18.2.0)
+ specifier: ^14.0.3
+ version: 14.0.3(react-dom@18.2.0)(react@18.2.0)
next-sanity:
specifier: ^4.2.0
- version: 4.3.3(@sanity/ui@1.9.0)(@types/styled-components@5.1.29)(next@13.5.6)(react@18.2.0)(sanity@3.19.1)(styled-components@5.3.11)
+ version: 4.3.3(@sanity/ui@1.9.0)(@types/styled-components@5.1.29)(next@14.0.3)(react@18.2.0)(sanity@3.19.1)(styled-components@5.3.11)
node-fetch:
specifier: ^3.2.10
version: 3.3.2
@@ -171,7 +171,7 @@ importers:
version: 3.19.1
'@storybook/nextjs':
specifier: ^7.4.0
- version: 7.5.3(@types/react-dom@18.2.0)(@types/react@18.2.0)(next@13.5.6)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)
+ version: 7.5.3(@types/react-dom@18.2.0)(@types/react@18.2.0)(next@14.0.3)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)
'@testing-library/jest-dom':
specifier: ^5.16.5
version: 5.17.0
@@ -3018,7 +3018,7 @@ packages:
'@motionone/easing': 10.16.3
'@motionone/types': 10.16.3
'@motionone/utils': 10.16.3
- tslib: 2.4.0
+ tslib: 2.6.2
dev: false
/@motionone/dom@10.16.4:
@@ -3029,14 +3029,14 @@ packages:
'@motionone/types': 10.16.3
'@motionone/utils': 10.16.3
hey-listen: 1.0.8
- tslib: 2.4.0
+ tslib: 2.6.2
dev: false
/@motionone/easing@10.16.3:
resolution: {integrity: sha512-HWTMZbTmZojzwEuKT/xCdvoMPXjYSyQvuVM6jmM0yoGU6BWzsmYMeB4bn38UFf618fJCNtP9XeC/zxtKWfbr0w==}
dependencies:
'@motionone/utils': 10.16.3
- tslib: 2.4.0
+ tslib: 2.6.2
dev: false
/@motionone/generators@10.16.4:
@@ -3044,7 +3044,7 @@ packages:
dependencies:
'@motionone/types': 10.16.3
'@motionone/utils': 10.16.3
- tslib: 2.4.0
+ tslib: 2.6.2
dev: false
/@motionone/types@10.16.3:
@@ -3056,7 +3056,7 @@ packages:
dependencies:
'@motionone/types': 10.16.3
hey-listen: 1.0.8
- tslib: 2.4.0
+ tslib: 2.6.2
dev: false
/@mswjs/cookies@0.2.2:
@@ -3091,8 +3091,8 @@ packages:
tar-fs: 2.1.1
dev: true
- /@next/env@13.5.6:
- resolution: {integrity: sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==}
+ /@next/env@14.0.3:
+ resolution: {integrity: sha512-7xRqh9nMvP5xrW4/+L0jgRRX+HoNRGnfJpD+5Wq6/13j3dsdzxO3BCXn7D3hMqsDb+vjZnJq+vI7+EtgrYZTeA==}
/@next/eslint-plugin-next@12.2.5:
resolution: {integrity: sha512-VBjVbmqEzGiOTBq4+wpeVXt/KgknnGB6ahvC/AxiIGnN93/RCSyXhFRI4uSfftM2Ba3w7ZO7076bfKasZsA0fw==}
@@ -3100,72 +3100,72 @@ packages:
glob: 7.1.7
dev: true
- /@next/swc-darwin-arm64@13.5.6:
- resolution: {integrity: sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==}
+ /@next/swc-darwin-arm64@14.0.3:
+ resolution: {integrity: sha512-64JbSvi3nbbcEtyitNn2LEDS/hcleAFpHdykpcnrstITFlzFgB/bW0ER5/SJJwUPj+ZPY+z3e+1jAfcczRLVGw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
optional: true
- /@next/swc-darwin-x64@13.5.6:
- resolution: {integrity: sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==}
+ /@next/swc-darwin-x64@14.0.3:
+ resolution: {integrity: sha512-RkTf+KbAD0SgYdVn1XzqE/+sIxYGB7NLMZRn9I4Z24afrhUpVJx6L8hsRnIwxz3ERE2NFURNliPjJ2QNfnWicQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
requiresBuild: true
optional: true
- /@next/swc-linux-arm64-gnu@13.5.6:
- resolution: {integrity: sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==}
+ /@next/swc-linux-arm64-gnu@14.0.3:
+ resolution: {integrity: sha512-3tBWGgz7M9RKLO6sPWC6c4pAw4geujSwQ7q7Si4d6bo0l6cLs4tmO+lnSwFp1Tm3lxwfMk0SgkJT7EdwYSJvcg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
requiresBuild: true
optional: true
- /@next/swc-linux-arm64-musl@13.5.6:
- resolution: {integrity: sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==}
+ /@next/swc-linux-arm64-musl@14.0.3:
+ resolution: {integrity: sha512-v0v8Kb8j8T23jvVUWZeA2D8+izWspeyeDGNaT2/mTHWp7+37fiNfL8bmBWiOmeumXkacM/AB0XOUQvEbncSnHA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
requiresBuild: true
optional: true
- /@next/swc-linux-x64-gnu@13.5.6:
- resolution: {integrity: sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==}
+ /@next/swc-linux-x64-gnu@14.0.3:
+ resolution: {integrity: sha512-VM1aE1tJKLBwMGtyBR21yy+STfl0MapMQnNrXkxeyLs0GFv/kZqXS5Jw/TQ3TSUnbv0QPDf/X8sDXuMtSgG6eg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
requiresBuild: true
optional: true
- /@next/swc-linux-x64-musl@13.5.6:
- resolution: {integrity: sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==}
+ /@next/swc-linux-x64-musl@14.0.3:
+ resolution: {integrity: sha512-64EnmKy18MYFL5CzLaSuUn561hbO1Gk16jM/KHznYP3iCIfF9e3yULtHaMy0D8zbHfxset9LTOv6cuYKJgcOxg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
requiresBuild: true
optional: true
- /@next/swc-win32-arm64-msvc@13.5.6:
- resolution: {integrity: sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==}
+ /@next/swc-win32-arm64-msvc@14.0.3:
+ resolution: {integrity: sha512-WRDp8QrmsL1bbGtsh5GqQ/KWulmrnMBgbnb+59qNTW1kVi1nG/2ndZLkcbs2GX7NpFLlToLRMWSQXmPzQm4tog==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
requiresBuild: true
optional: true
- /@next/swc-win32-ia32-msvc@13.5.6:
- resolution: {integrity: sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==}
+ /@next/swc-win32-ia32-msvc@14.0.3:
+ resolution: {integrity: sha512-EKffQeqCrj+t6qFFhIFTRoqb2QwX1mU7iTOvMyLbYw3QtqTw9sMwjykyiMlZlrfm2a4fA84+/aeW+PMg1MjuTg==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
requiresBuild: true
optional: true
- /@next/swc-win32-x64-msvc@13.5.6:
- resolution: {integrity: sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==}
+ /@next/swc-win32-x64-msvc@14.0.3:
+ resolution: {integrity: sha512-ERhKPSJ1vQrPiwrs15Pjz/rvDHZmkmvbf/BjPN/UCOI++ODftT0GtasDPi0j+y6PPJi5HsXw+dpRaXUaw4vjuQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -5203,7 +5203,7 @@ packages:
resolution: {integrity: sha512-TXJJd5RAKakWx4BtpwvSNdgTDkKM6RkXU8GK34S/LhidQ5Pjz3wcnqb0TxEkfhK/ztbP8nKHqXFwLfa2CYkvQw==}
dev: true
- /@storybook/nextjs@7.5.3(@types/react-dom@18.2.0)(@types/react@18.2.0)(next@13.5.6)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2):
+ /@storybook/nextjs@7.5.3(@types/react-dom@18.2.0)(@types/react@18.2.0)(next@14.0.3)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2):
resolution: {integrity: sha512-PYi9AJga6x46IN4aub9CuiKNF9mT3maTh1F9dXqE4kO+ZrbesiKcJ3Uud0D78c56/Jlr8FmHEDpO19OlgRM4kQ==}
engines: {node: '>=16.0.0'}
peerDependencies:
@@ -5250,7 +5250,7 @@ packages:
fs-extra: 11.1.1
image-size: 1.0.2
loader-utils: 3.2.1
- next: 13.5.6(react-dom@18.2.0)(react@18.2.0)
+ next: 14.0.3(react-dom@18.2.0)(react@18.2.0)
node-polyfill-webpack-plugin: 2.0.1
pnp-webpack-plugin: 1.7.0(typescript@5.2.2)
postcss: 8.4.31
@@ -13968,7 +13968,7 @@ packages:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
dev: true
- /next-sanity@4.3.3(@sanity/ui@1.9.0)(@types/styled-components@5.1.29)(next@13.5.6)(react@18.2.0)(sanity@3.19.1)(styled-components@5.3.11):
+ /next-sanity@4.3.3(@sanity/ui@1.9.0)(@types/styled-components@5.1.29)(next@14.0.3)(react@18.2.0)(sanity@3.19.1)(styled-components@5.3.11):
resolution: {integrity: sha512-537xLC4hpTgV3SGj6w4aXvBvm/nHdyZGtZ5IcZpH33p70J7UjptF4+D4FvWKZGPjF3v3SFk75AHjxWVPORL9RA==}
engines: {node: '>=16'}
peerDependencies:
@@ -13987,7 +13987,7 @@ packages:
'@sanity/webhook': 2.0.0
'@types/styled-components': 5.1.29
groq: 3.19.1
- next: 13.5.6(react-dom@18.2.0)(react@18.2.0)
+ next: 14.0.3(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
sanity: 3.19.1(@types/node@20.8.7)(@types/react@18.2.0)(react-dom@18.2.0)(react@18.2.0)(styled-components@5.3.11)
styled-components: 5.3.11(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)
@@ -13995,9 +13995,9 @@ packages:
- supports-color
dev: false
- /next@13.5.6(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==}
- engines: {node: '>=16.14.0'}
+ /next@14.0.3(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-AbYdRNfImBr3XGtvnwOxq8ekVCwbFTv/UJoLwmaX89nk9i051AEY4/HAWzU0YpaTDw8IofUpmuIlvzWF13jxIw==}
+ engines: {node: '>=18.17.0'}
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
@@ -14010,7 +14010,7 @@ packages:
sass:
optional: true
dependencies:
- '@next/env': 13.5.6
+ '@next/env': 14.0.3
'@swc/helpers': 0.5.2
busboy: 1.6.0
caniuse-lite: 1.0.30001561
@@ -14020,15 +14020,15 @@ packages:
styled-jsx: 5.1.1(@babel/core@7.23.2)(react@18.2.0)
watchpack: 2.4.0
optionalDependencies:
- '@next/swc-darwin-arm64': 13.5.6
- '@next/swc-darwin-x64': 13.5.6
- '@next/swc-linux-arm64-gnu': 13.5.6
- '@next/swc-linux-arm64-musl': 13.5.6
- '@next/swc-linux-x64-gnu': 13.5.6
- '@next/swc-linux-x64-musl': 13.5.6
- '@next/swc-win32-arm64-msvc': 13.5.6
- '@next/swc-win32-ia32-msvc': 13.5.6
- '@next/swc-win32-x64-msvc': 13.5.6
+ '@next/swc-darwin-arm64': 14.0.3
+ '@next/swc-darwin-x64': 14.0.3
+ '@next/swc-linux-arm64-gnu': 14.0.3
+ '@next/swc-linux-arm64-musl': 14.0.3
+ '@next/swc-linux-x64-gnu': 14.0.3
+ '@next/swc-linux-x64-musl': 14.0.3
+ '@next/swc-win32-arm64-msvc': 14.0.3
+ '@next/swc-win32-ia32-msvc': 14.0.3
+ '@next/swc-win32-x64-msvc': 14.0.3
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros