From 23d15496d10da1942f556fc64cd04b9324b63515 Mon Sep 17 00:00:00 2001 From: Lee Robinson Date: Tue, 9 May 2023 07:21:15 -0700 Subject: [PATCH 01/62] Add Saleor provider. (#1008) --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index e7ec645ad..22420bf64 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ A Next.js 13 and App Router-ready ecommerce template, built with Shopify, featur - Checkout and payments with Shopify - Automatic light/dark mode based on system settings +## Alternate Providers + +- [Saleor](https://github.com/saleor/nextjs-commerce) ([Demo](https://saleor-commerce.vercel.app/)) + ## Running locally You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js Commerce. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables) for this, but a `.env` file is all that is necessary. From a5e799b16e24cd56f9f98e65d6887600349c1837 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Tue, 9 May 2023 22:18:01 -0400 Subject: [PATCH 02/62] Use parallel fetches for sitemap requests and remove duplicate /search url (#1004) Co-authored-by: Andrew Jones Co-authored-by: Lee Robinson --- app/sitemap.ts | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/app/sitemap.ts b/app/sitemap.ts index 1a0fd8232..d8cdfd2ea 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -6,28 +6,35 @@ const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL : 'http://localhost:3000'; export default async function sitemap(): Promise>> { - const routesMap = ['', '/search'].map((route) => ({ + const routesMap = [''].map((route) => ({ url: `${baseUrl}${route}`, lastModified: new Date().toISOString() })); - const collections = await getCollections(); - const collectionsMap = collections.map((collection) => ({ - url: `${baseUrl}${collection.path}`, - lastModified: collection.updatedAt - })); + const collectionsPromise = getCollections().then((collections) => + collections.map((collection) => ({ + url: `${baseUrl}${collection.path}`, + lastModified: collection.updatedAt + })) + ); - const products = await getProducts({}); - const productsMap = products.map((product) => ({ - url: `${baseUrl}/product/${product.handle}`, - lastModified: product.updatedAt - })); + const productsPromise = getProducts({}).then((products) => + products.map((product) => ({ + url: `${baseUrl}/product/${product.handle}`, + lastModified: product.updatedAt + })) + ); - const pages = await getPages(); - const pagesMap = pages.map((page) => ({ - url: `${baseUrl}/${page.handle}`, - lastModified: page.updatedAt - })); + const pagesPromise = getPages().then((pages) => + pages.map((page) => ({ + url: `${baseUrl}/${page.handle}`, + lastModified: page.updatedAt + })) + ); - return [...routesMap, ...collectionsMap, ...productsMap, ...pagesMap]; + const fetchedRoutes = ( + await Promise.all([collectionsPromise, productsPromise, pagesPromise]) + ).flat(); + + return [...routesMap, ...fetchedRoutes]; } From a0c0d10faef7622bbfafebbf0b3a5f148a95d612 Mon Sep 17 00:00:00 2001 From: Michael Novotny Date: Thu, 11 May 2023 12:53:04 -0700 Subject: [PATCH 03/62] Changes mobile menu animation to be consistent with cart animation. (#1015) --- components/layout/navbar/mobile-menu.tsx | 110 +++++++++++++---------- 1 file changed, 65 insertions(+), 45 deletions(-) diff --git a/components/layout/navbar/mobile-menu.tsx b/components/layout/navbar/mobile-menu.tsx index c960d5218..a12759c89 100644 --- a/components/layout/navbar/mobile-menu.tsx +++ b/components/layout/navbar/mobile-menu.tsx @@ -1,7 +1,7 @@ 'use client'; import { Dialog } from '@headlessui/react'; -import { motion } from 'framer-motion'; +import { AnimatePresence, motion } from 'framer-motion'; import Link from 'next/link'; import { usePathname, useSearchParams } from 'next/navigation'; import { useEffect, useState } from 'react'; @@ -42,57 +42,77 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) { > - { - setMobileMenuIsOpen(false); - }} - className="relative z-50" - > -
- + {mobileMenuIsOpen && ( + { + setMobileMenuIsOpen(false); }} - className="flex w-full flex-col bg-white pb-6 dark:bg-black" + className="relative z-50" > -
- +
+ -
- -
- {menu.length ? ( -
    - {menu.map((item: Menu) => ( -
  • - { - setMobileMenuIsOpen(false); - }} - > - {item.title} - -
  • - ))} -
- ) : null} +
+ +
+ {menu.length ? ( +
    + {menu.map((item: Menu) => ( +
  • + { + setMobileMenuIsOpen(false); + }} + > + {item.title} + +
  • + ))} +
+ ) : null} +
+
-
-
-
+ + )} + ); } From f5dade74fb149a507dcdd74772f48055b77cbc06 Mon Sep 17 00:00:00 2001 From: Michael Novotny Date: Fri, 12 May 2023 16:02:51 -0700 Subject: [PATCH 04/62] Fixes search page bugs. (#1019) --- app/search/[collection]/page.tsx | 13 +++++++++++-- components/carousel.tsx | 2 +- components/grid/three-items.tsx | 4 +++- components/layout/navbar/search.tsx | 8 ++++++-- components/layout/search/filter/item.tsx | 14 ++++++++++++-- lib/shopify/index.ts | 16 +++++++++++++--- lib/shopify/queries/collection.ts | 8 ++++++-- lib/shopify/types.ts | 2 ++ 8 files changed, 54 insertions(+), 13 deletions(-) diff --git a/app/search/[collection]/page.tsx b/app/search/[collection]/page.tsx index 0e1782b7d..873701bf7 100644 --- a/app/search/[collection]/page.tsx +++ b/app/search/[collection]/page.tsx @@ -4,6 +4,7 @@ import { notFound } from 'next/navigation'; import Grid from 'components/grid'; import ProductGridItems from 'components/layout/product-grid-items'; +import { defaultSort, sorting } from 'lib/constants'; export const runtime = 'edge'; @@ -32,8 +33,16 @@ export async function generateMetadata({ }; } -export default async function CategoryPage({ params }: { params: { collection: string } }) { - const products = await getCollectionProducts(params.collection); +export default async function CategoryPage({ + params, + searchParams +}: { + params: { collection: string }; + searchParams?: { [key: string]: string | string[] | undefined }; +}) { + const { sort } = searchParams as { [key: string]: string }; + const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort; + const products = await getCollectionProducts({ collection: params.collection, sortKey, reverse }); return (
diff --git a/components/carousel.tsx b/components/carousel.tsx index 30b6a4733..d86d17f45 100644 --- a/components/carousel.tsx +++ b/components/carousel.tsx @@ -4,7 +4,7 @@ import Link from 'next/link'; export async function Carousel() { // Collections that start with `hidden-*` are hidden from the search page. - const products = await getCollectionProducts('hidden-homepage-carousel'); + const products = await getCollectionProducts({ collection: 'hidden-homepage-carousel' }); if (!products?.length) return null; diff --git a/components/grid/three-items.tsx b/components/grid/three-items.tsx index 6814a171a..2280de26e 100644 --- a/components/grid/three-items.tsx +++ b/components/grid/three-items.tsx @@ -37,7 +37,9 @@ function ThreeItemGridItem({ export async function ThreeItemGrid() { // Collections that start with `hidden-*` are hidden from the search page. - const homepageItems = await getCollectionProducts('hidden-homepage-featured-items'); + const homepageItems = await getCollectionProducts({ + collection: 'hidden-homepage-featured-items' + }); if (!homepageItems[0] || !homepageItems[1] || !homepageItems[2]) return null; diff --git a/components/layout/navbar/search.tsx b/components/layout/navbar/search.tsx index b3ff9a6bb..fff895f43 100644 --- a/components/layout/navbar/search.tsx +++ b/components/layout/navbar/search.tsx @@ -3,6 +3,7 @@ import { useRouter, useSearchParams } from 'next/navigation'; import SearchIcon from 'components/icons/search'; +import { createUrl } from 'lib/utils'; export default function Search() { const router = useRouter(); @@ -13,12 +14,15 @@ export default function Search() { const val = e.target as HTMLFormElement; const search = val.search as HTMLInputElement; + const newParams = new URLSearchParams(searchParams.toString()); if (search.value) { - router.push(`/search?q=${search.value}`); + newParams.set('q', search.value); } else { - router.push(`/search`); + newParams.delete('q'); } + + router.push(createUrl('/search', newParams)); } return ( diff --git a/components/layout/search/filter/item.tsx b/components/layout/search/filter/item.tsx index f56d6114b..1a3b73ad3 100644 --- a/components/layout/search/filter/item.tsx +++ b/components/layout/search/filter/item.tsx @@ -12,6 +12,9 @@ function PathFilterItem({ item }: { item: PathFilterItem }) { const pathname = usePathname(); const searchParams = useSearchParams(); const [active, setActive] = useState(pathname === item.path); + const newParams = new URLSearchParams(searchParams.toString()); + + newParams.delete('q'); useEffect(() => { setActive(pathname === item.path); @@ -20,7 +23,7 @@ function PathFilterItem({ item }: { item: PathFilterItem }) { return (
  • { setActive(searchParams.get('sort') === item.slug); @@ -43,7 +47,13 @@ function SortFilterItem({ item }: { item: SortFilterItem }) { const href = item.slug && item.slug.length - ? createUrl(pathname, new URLSearchParams({ sort: item.slug })) + ? createUrl( + pathname, + new URLSearchParams({ + ...(q && { q }), + sort: item.slug + }) + ) : pathname; return ( diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts index a2f45a36f..5d3972536 100644 --- a/lib/shopify/index.ts +++ b/lib/shopify/index.ts @@ -257,16 +257,26 @@ export async function getCollection(handle: string): Promise { +export async function getCollectionProducts({ + collection, + reverse, + sortKey +}: { + collection: string; + reverse?: boolean; + sortKey?: string; +}): Promise { const res = await shopifyFetch({ query: getCollectionProductsQuery, variables: { - handle + handle: collection, + reverse, + sortKey } }); if (!res.body.data.collection) { - console.log('No collection found for handle', handle); + console.log(`No collection found for \`${collection}\``); return []; } diff --git a/lib/shopify/queries/collection.ts b/lib/shopify/queries/collection.ts index ac3fb4dd9..6396ff8eb 100644 --- a/lib/shopify/queries/collection.ts +++ b/lib/shopify/queries/collection.ts @@ -37,9 +37,13 @@ export const getCollectionsQuery = /* GraphQL */ ` `; export const getCollectionProductsQuery = /* GraphQL */ ` - query getCollectionProducts($handle: String!) { + query getCollectionProducts( + $handle: String! + $sortKey: ProductCollectionSortKeys + $reverse: Boolean + ) { collection(handle: $handle) { - products(first: 100) { + products(sortKey: $sortKey, reverse: $reverse, first: 100) { edges { node { ...product diff --git a/lib/shopify/types.ts b/lib/shopify/types.ts index b18ed3b63..23dc02d46 100644 --- a/lib/shopify/types.ts +++ b/lib/shopify/types.ts @@ -201,6 +201,8 @@ export type ShopifyCollectionProductsOperation = { }; variables: { handle: string; + reverse?: boolean; + sortKey?: string; }; }; From 50b4e6cbc6f0e50d3e2eeaf5d754f208a30b82a7 Mon Sep 17 00:00:00 2001 From: Lee Robinson Date: Mon, 15 May 2023 13:36:58 -0700 Subject: [PATCH 05/62] Update README for Medusa (#1020) * Update README.md * Prettier. --------- Co-authored-by: Michael Novotny --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 22420bf64..51cd1eca7 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,7 @@ # Next.js Commerce -> Note: Looking for Next.js Commerce v1? View the [code](https://github.com/vercel/commerce/tree/v1), [demo](https://commerce-v1.vercel.store), and [release notes](https://github.com/vercel/commerce/releases/tag/v1) - -A Next.js 13 and App Router-ready ecommerce template, built with Shopify, featuring: +A Next.js 13 and App Router-ready ecommerce template featuring: - Next.js App Router - Optimized for SEO using Next.js's Metadata @@ -17,9 +15,13 @@ A Next.js 13 and App Router-ready ecommerce template, built with Shopify, featur - Checkout and payments with Shopify - Automatic light/dark mode based on system settings -## Alternate Providers +> Note: Looking for Next.js Commerce v1? View the [code](https://github.com/vercel/commerce/tree/v1), [demo](https://commerce-v1.vercel.store), and [release notes](https://github.com/vercel/commerce/releases/tag/v1) +## Providers + +- Shopify (this repository) - [Saleor](https://github.com/saleor/nextjs-commerce) ([Demo](https://saleor-commerce.vercel.app/)) +- [Medusa](https://github.com/medusajs/vercel-commerce) ([Demo](https://medusa-nextjs-commerce.vercel.app/)) ## Running locally From 30a080182c325079e97cd813326b240e43d016de Mon Sep 17 00:00:00 2001 From: "Abhushan A. Joshi" <49617450+abhu-A-J@users.noreply.github.com> Date: Mon, 22 May 2023 22:34:27 +0530 Subject: [PATCH 06/62] Adds a basic product JSON-LD schema on product details page. (#1016) --- app/product/[handle]/page.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/product/[handle]/page.tsx b/app/product/[handle]/page.tsx index 612cd0236..69252efc2 100644 --- a/app/product/[handle]/page.tsx +++ b/app/product/[handle]/page.tsx @@ -58,8 +58,31 @@ export default async function ProductPage({ params }: { params: { handle: string if (!product) return notFound(); + const productJsonLd = { + '@context': 'https://schema.org', + '@type': 'Product', + name: product.title, + description: product.description, + image: product.featuredImage.url, + offers: { + '@type': 'AggregateOffer', + availability: product.availableForSale + ? 'https://schema.org/InStock' + : 'https://schema.org/OutOfStock', + priceCurrency: product.priceRange.minVariantPrice.currencyCode, + highPrice: product.priceRange.maxVariantPrice.amount, + lowPrice: product.priceRange.minVariantPrice.amount + } + }; + return (
    +