From d97b7a64e3c3562274fa5a68d4fb805086d0d2c6 Mon Sep 17 00:00:00 2001 From: Nathan Kluth Date: Wed, 6 Dec 2023 09:25:47 -0700 Subject: [PATCH] Feature/app router (#213) * wip * fonts * page animations * mv * wip * middleware * mv * wip * fix link * slug server page * fix * ts * single * slug from params * e2e test updates w/o serversideprops * warnings * cleanup * e2e node version * wait * shared var * remove old config val --- .github/workflows/e2e-tests.yml | 2 +- README.md | 2 +- packages/nextjs/app/about/page.tsx | 5 + packages/nextjs/app/categories/page.tsx | 19 ++++ packages/nextjs/app/global.css | 84 ++++++++++++++++- packages/nextjs/app/layout.tsx | 32 ++++++- .../nextjs/{pages => app/migration}/about.tsx | 10 +- .../{pages => app/migration}/categories.tsx | 22 +---- .../{pages => app/migration}/components.tsx | 3 + .../app/migration/getOrderingFromQuery.ts | 22 +++++ .../index.tsx => app/migration/home-page.tsx} | 31 +------ .../migration}/products/[slug].tsx | 60 +++--------- .../migration}/products/index.tsx | 70 ++------------ packages/nextjs/app/migration/sorting.ts | 52 +++++++++++ packages/nextjs/app/page.tsx | 28 ++++++ packages/nextjs/app/products/[slug]/page.tsx | 20 ++++ packages/nextjs/app/products/page.tsx | 58 ++++++++++++ packages/nextjs/app/template.tsx | 8 ++ packages/nextjs/app/ui/framer.tsx | 3 + packages/nextjs/app/ui/shared-ui.ts | 5 + packages/nextjs/components/Breadcrumbs.tsx | 10 +- packages/nextjs/components/CartContext.tsx | 2 + packages/nextjs/components/Header/Header.tsx | 6 +- packages/nextjs/components/Pagination.tsx | 11 ++- .../components/ProductFilters/FilterGroup.tsx | 7 +- .../ProductPage/ProductVariantSelector.tsx | 7 +- .../components/ProductPage/StyleOptions.tsx | 2 +- packages/nextjs/components/ProductSort.tsx | 4 +- packages/nextjs/middleware.ts | 44 +++++++++ packages/nextjs/next.config.js | 3 - packages/nextjs/package.json | 6 +- packages/nextjs/pages/_app.tsx | 38 -------- packages/nextjs/pages/_document.tsx | 18 ---- packages/nextjs/styles/global.css | 42 --------- packages/nextjs/tailwind.config.js | 1 + packages/nextjs/utils/getFiltersCount.ts | 12 +-- packages/nextjs/utils/getProductBySlug.ts | 70 +++++++------- .../nextjs/utils/groqTypes/ProductDetail.ts | 7 ++ .../nextjs/utils/groqTypes/ProductList.ts | 2 +- packages/nextjs/utils/setCachingHeaders.ts | 15 --- packages/nextjs/utils/useRouterQueryParams.ts | 53 ++++------- .../components/ProductSort/ProductSort.tsx | 1 + packages/shared-ui/components/Select.tsx | 4 +- packages/shared-ui/components/cart/Cart.tsx | 2 +- .../components/cart/stories/Cart.stories.tsx | 1 + .../support/commands/getServerSideProps.ts | 50 ---------- .../cypress/support/commands/index.ts | 1 - packages/tests-e2e/e2e-tests/categories.cy.ts | 14 +-- packages/tests-e2e/e2e-tests/home.cy.ts | 23 ++--- packages/tests-e2e/e2e-tests/product.cy.ts | 28 ++---- packages/tests-e2e/e2e-tests/products.cy.ts | 34 ++----- pnpm-lock.yaml | 92 +++++++++---------- 52 files changed, 600 insertions(+), 546 deletions(-) create mode 100644 packages/nextjs/app/about/page.tsx create mode 100644 packages/nextjs/app/categories/page.tsx rename packages/nextjs/{pages => app/migration}/about.tsx (98%) rename packages/nextjs/{pages => app/migration}/categories.tsx (57%) rename packages/nextjs/{pages => app/migration}/components.tsx (98%) create mode 100644 packages/nextjs/app/migration/getOrderingFromQuery.ts rename packages/nextjs/{pages/index.tsx => app/migration/home-page.tsx} (77%) rename packages/nextjs/{pages => app/migration}/products/[slug].tsx (72%) rename packages/nextjs/{pages => app/migration}/products/index.tsx (65%) create mode 100644 packages/nextjs/app/migration/sorting.ts create mode 100644 packages/nextjs/app/page.tsx create mode 100644 packages/nextjs/app/products/[slug]/page.tsx create mode 100644 packages/nextjs/app/products/page.tsx create mode 100644 packages/nextjs/app/template.tsx create mode 100644 packages/nextjs/app/ui/framer.tsx create mode 100644 packages/nextjs/app/ui/shared-ui.ts create mode 100644 packages/nextjs/middleware.ts delete mode 100644 packages/nextjs/pages/_app.tsx delete mode 100644 packages/nextjs/pages/_document.tsx delete mode 100644 packages/nextjs/styles/global.css create mode 100644 packages/nextjs/utils/groqTypes/ProductDetail.ts delete mode 100644 packages/nextjs/utils/setCachingHeaders.ts delete mode 100644 packages/tests-e2e/cypress/support/commands/getServerSideProps.ts 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

    = (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 = ({