Merge branch 'main' into update-next

This commit is contained in:
Catalin Pinte 2022-12-09 09:10:28 +02:00
commit 04eb717e2c
20 changed files with 193 additions and 41 deletions

View File

@ -20,11 +20,11 @@ Demo live at: [demo.vercel.store](https://demo.vercel.store/)
## Run minimal version locally ## Run minimal version locally
> To run a minimal version of Next.js Commerce you can start with the default local provider `@vercel/commerce-local` that has disabled all features (cart, auth) and use static files for the backend > To run a minimal version of Next.js Commerce you can start with the default local provider `@vercel/commerce-local` that has all features disabled (cart, auth) and uses static files for the backend
```bash ```bash
pnpm install & pnpm build # run this commands in root folder of the mono repo pnpm install & pnpm build # run these commands in the root folder of the mono repo
pnpm dev # run this commands in the site folder pnpm dev # run this command in the site folder
``` ```
> If you encounter any problems while installing and running for the first time, please see the Troubleshoot section > If you encounter any problems while installing and running for the first time, please see the Troubleshoot section
@ -47,10 +47,10 @@ Next.js Commerce integrates out-of-the-box with BigCommerce, Shopify, Swell, Sal
## Considerations ## Considerations
- `packages/commerce` contains all types, helpers and functions to be used as base to build a new **provider**. - `packages/commerce` contains all types, helpers and functions to be used as a base to build a new **provider**.
- **Providers** live under `packages`'s root folder and they will extend Next.js Commerce types and functionality (`packages/commerce`). - **Providers** live under `packages`'s root folder and they will extend Next.js Commerce types and functionality (`packages/commerce`).
- We have a **Features API** to ensure feature parity between the UI and the Provider. The UI should update accordingly and no extra code should be bundled. All extra configuration for features will live under `features` in `commerce.config.json` and if needed it can also be accessed programatically. - We have a **Features API** to ensure feature parity between the UI and the Provider. The UI should update accordingly and no extra code should be bundled. All extra configuration for features will live under `features` in `commerce.config.json` and if needed it can also be accessed programmatically.
- Each **provider** should add its corresponding `next.config.js` and `commerce.config.json` adding specific data related to the provider. For example in case of BigCommerce, the images CDN and additional API routes. - Each **provider** should add its corresponding `next.config.js` and `commerce.config.json` adding specific data related to the provider. For example in the case of BigCommerce, the images CDN and additional API routes.
## Configuration ## Configuration
@ -73,7 +73,7 @@ Every provider defines the features that it supports under `packages/{provider}/
#### Features Available #### Features Available
The following features can be enabled or disabled. This means that the UI will remove all code related to the feature. The following features can be enabled or disabled. This means that the UI will remove all code related to the feature.
For example: Turning `cart` off will disable Cart capabilities. For example: turning `cart` off will disable Cart capabilities.
- cart - cart
- search - search
@ -83,7 +83,7 @@ For example: Turning `cart` off will disable Cart capabilities.
#### How to turn Features on and off #### How to turn Features on and off
> NOTE: The selected provider should support the feature that you are toggling. (This means that you can't turn wishlist on if the provider doesn't support this functionality out the box) > NOTE: The selected provider should support the feature that you are toggling. (This means that you can't turn wishlist on if the provider doesn't support this functionality out of the box)
- Open `site/commerce.config.json` - Open `site/commerce.config.json`
- You'll see a config file like this: - You'll see a config file like this:

View File

@ -42,7 +42,9 @@ export class CommerceNetworkError extends Error {
} }
export const normalizeZodIssues = (issues: ZodError['issues']) => export const normalizeZodIssues = (issues: ZodError['issues']) =>
issues.map(({ path, message }) => `${message} at "${path.join('.')}" field`) issues.map(({ path, message }) =>
path.length ? `${message} at "${path.join('.')}" field` : message
)
export const getOperationError = (operation: string, error: unknown) => { export const getOperationError = (operation: string, error: unknown) => {
if (error instanceof ZodError) { if (error instanceof ZodError) {

View File

@ -21,7 +21,7 @@ export const withOperationCallback =
const parse = ({ name, data }: Operation) => { const parse = ({ name, data }: Operation) => {
switch (name) { switch (name) {
case 'getProduct': case 'getProduct':
productSchema.nullable().parse(data.product) productSchema.optional().parse(data.product)
break break
case 'getAllProducts': case 'getAllProducts':
z.array(productSchema).parse(data.products) z.array(productSchema).parse(data.products)

View File

@ -27,7 +27,7 @@ export default function getSiteInfoOperation({
const { sdkFetch } = commerce.getConfig(config) const { sdkFetch } = commerce.getConfig(config)
const { data: categories } = await sdkFetch('categories', 'list') const { data: categories } = await sdkFetch('categories', 'list')
const formattedCategories = categories.map(normalizeCategory) const formattedCategories = categories?.map(normalizeCategory) ?? []
return { return {
categories: formattedCategories, categories: formattedCategories,

View File

@ -22,17 +22,17 @@ export const handler: MutationHook<AddItemHook> = {
variables.push(item.variantId) variables.push(item.variantId)
} }
const { cart } = await fetch<{ cart: CommercejsCart }>({ const cart = await fetch<CommercejsCart>({
query: options.query, query: options.query,
method: options.method, method: options.method,
variables, variables,
}) })
return normalizeCart(cart) return normalizeCart(cart)
}, },
useHook: ({ fetch }) => useHook: ({ fetch }) =>
function useHook() { function useHook() {
const { mutate } = useCart() const { mutate } = useCart()
return useCallback( return useCallback(
async function addItem(input) { async function addItem(input) {
const cart = await fetch({ input }) const cart = await fetch({ input })

View File

@ -16,7 +16,7 @@ export const handler: MutationHook<RemoveItemHook> = {
method: 'remove', method: 'remove',
}, },
async fetcher({ input, options, fetch }) { async fetcher({ input, options, fetch }) {
const { cart } = await fetch<{ cart: CommercejsCart }>({ const cart = await fetch<CommercejsCart>({
query: options.query, query: options.query,
method: options.method, method: options.method,
variables: input.itemId, variables: input.itemId,

View File

@ -30,7 +30,7 @@ export const handler = {
}, },
async fetcher({ input, options, fetch }: HookFetcherContext<UpdateItemHook>) { async fetcher({ input, options, fetch }: HookFetcherContext<UpdateItemHook>) {
const variables = [input.itemId, { quantity: input.item.quantity }] const variables = [input.itemId, { quantity: input.item.quantity }]
const { cart } = await fetch<{ cart: CommercejsCart }>({ const cart = await fetch<CommercejsCart>({
query: options.query, query: options.query,
method: options.method, method: options.method,
variables, variables,
@ -57,7 +57,7 @@ export const handler = {
const variantId = input.productId ?? item?.variantId const variantId = input.productId ?? item?.variantId
const quantity = input?.quantity ?? item?.quantity const quantity = input?.quantity ?? item?.quantity
if (!itemId || !productId || !variantId) { if (!itemId || !productId) {
throw new ValidationError({ throw new ValidationError({
message: 'Invalid input for updating cart item', message: 'Invalid input for updating cart item',
}) })
@ -69,7 +69,7 @@ export const handler = {
item: { item: {
quantity, quantity,
productId, productId,
variantId, variantId: variantId ?? '',
}, },
}, },
}) })

View File

@ -44,14 +44,16 @@ const normalizeLineItem = (
} }
} }
export const normalizeCart = (commercejsCart: CommercejsCart): Cart => { export const normalizeCart = (
commercejsCart: CommercejsCart | { cart: CommercejsCart }
): Cart => {
const { const {
id, id,
created, created,
subtotal: { raw: rawPrice }, subtotal: { raw: rawPrice },
currency, currency,
line_items, line_items,
} = commercejsCart } = 'cart' in commercejsCart ? commercejsCart.cart : commercejsCart
return { return {
id, id,

View File

@ -54,6 +54,7 @@ export function normalizeProduct(
): Product { ): Product {
const { id, name, description, permalink, assets, price, variant_groups } = const { id, name, description, permalink, assets, price, variant_groups } =
commercejsProduct commercejsProduct
return { return {
id, id,
name, name,
@ -61,15 +62,19 @@ export function normalizeProduct(
descriptionHtml: description, descriptionHtml: description,
slug: permalink, slug: permalink,
path: `/${permalink}`, path: `/${permalink}`,
images: assets.map(({ url, description, filename }) => ({ images:
url, assets?.map(({ url, description, filename }) => ({
alt: description || filename, url,
})), alt: description || filename,
})) || [],
price: { price: {
value: price.raw, value: price.raw,
currencyCode: 'USD', currencyCode: 'USD',
}, },
variants: normalizeVariants(commercejsProductVariants, variant_groups), variants: normalizeVariants(
options: getOptionsFromVariantGroups(variant_groups), commercejsProductVariants,
variant_groups || []
),
options: variant_groups ? getOptionsFromVariantGroups(variant_groups) : [],
} }
} }

View File

@ -2,7 +2,6 @@ import type { GraphQLFetcher } from '@vercel/commerce/api'
import { API_URL } from '../../const' import { API_URL } from '../../const'
import { getError } from '../../utils/handle-fetch-response' import { getError } from '../../utils/handle-fetch-response'
import { getCommerceApi } from '..'
import { getToken } from '../../utils/index' import { getToken } from '../../utils/index'
const fetchGraphqlApi: GraphQLFetcher = async ( const fetchGraphqlApi: GraphQLFetcher = async (
@ -10,7 +9,6 @@ const fetchGraphqlApi: GraphQLFetcher = async (
{ variables } = {}, { variables } = {},
headers?: HeadersInit headers?: HeadersInit
) => { ) => {
const config = getCommerceApi().getConfig()
const token = getToken() const token = getToken()
const res = await fetch(API_URL!, { const res = await fetch(API_URL!, {
@ -28,10 +26,17 @@ const fetchGraphqlApi: GraphQLFetcher = async (
}), }),
}) })
const { data, errors, status } = await res.json() const { data, errors, message, type, status } = await res.json()
if (errors) { if (errors || res.status >= 400) {
throw getError(errors, status) throw getError(
errors || [
{
message: `${type ? `${type}, ` : ''}${message}`,
},
],
status || res.status
)
} }
return { data, res } return { data, res }

View File

@ -7,6 +7,7 @@ import getSlug from '@lib/get-slug'
import { Github, Vercel } from '@components/icons' import { Github, Vercel } from '@components/icons'
import { Logo, Container } from '@components/ui' import { Logo, Container } from '@components/ui'
import { I18nWidget } from '@components/common' import { I18nWidget } from '@components/common'
import ThemeSwitcher from '@components/ui/ThemeSwitcher'
import s from './Footer.module.css' import s from './Footer.module.css'
interface Props { interface Props {
@ -41,7 +42,7 @@ const Footer: FC<Props> = ({ className, pages }) => {
<span>ACME</span> <span>ACME</span>
</Link> </Link>
</div> </div>
<div className="col-span-1 lg:col-span-8"> <div className="col-span-1 lg:col-span-7">
<div className="grid md:grid-rows-4 md:grid-cols-3 md:grid-flow-col"> <div className="grid md:grid-rows-4 md:grid-cols-3 md:grid-flow-col">
{[...links, ...sitePages].map((page) => ( {[...links, ...sitePages].map((page) => (
<span key={page.url} className="py-3 md:py-0 md:pb-4"> <span key={page.url} className="py-3 md:py-0 md:pb-4">
@ -55,8 +56,10 @@ const Footer: FC<Props> = ({ className, pages }) => {
))} ))}
</div> </div>
</div> </div>
<div className="col-span-1 lg:col-span-2 flex items-start lg:justify-end text-primary"> <div className="col-span-1 lg:col-span-3 flex items-start lg:justify-end text-primary">
<div className="flex space-x-6 items-center h-10"> <div className="flex space-x-4 items-center h-10">
<ThemeSwitcher />
<I18nWidget />
<a <a
className={s.link} className={s.link}
aria-label="Github Repository" aria-label="Github Repository"
@ -64,7 +67,6 @@ const Footer: FC<Props> = ({ className, pages }) => {
> >
<Github /> <Github />
</a> </a>
<I18nWidget />
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@
} }
.button { .button {
@apply h-10 px-2 rounded-md border border-accent-2 flex items-center justify-center transition-colors ease-linear; @apply h-10 pl-2 pr-1 rounded-md border border-accent-2 flex items-center justify-center transition-colors ease-linear;
} }
.button:hover { .button:hover {
@ -32,7 +32,7 @@
} }
.icon.active { .icon.active {
transform: rotate(180deg); transform: rotate(90deg);
} }
@screen lg { @screen lg {

View File

@ -3,7 +3,7 @@ import Link from 'next/link'
import { FC, useState } from 'react' import { FC, useState } from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import s from './I18nWidget.module.css' import s from './I18nWidget.module.css'
import { Cross, ChevronUp } from '@components/icons' import { Cross, ChevronRight } from '@components/icons'
import ClickOutside from '@lib/click-outside' import ClickOutside from '@lib/click-outside'
import Image from 'next/image' import Image from 'next/image'
interface LOCALE_DATA { interface LOCALE_DATA {
@ -54,14 +54,14 @@ const I18nWidget: FC = () => {
<Image <Image
width="20" width="20"
height="20" height="20"
className="block mr-2 w-5" className="block w-5"
src={`/${LOCALES_MAP[currentLocale].img.filename}`} src={`/${LOCALES_MAP[currentLocale].img.filename}`}
alt={LOCALES_MAP[currentLocale].img.alt} alt={LOCALES_MAP[currentLocale].img.alt}
unoptimized unoptimized
/> />
{options && ( {options && (
<span className="cursor-pointer"> <span className="cursor-pointer ml-1">
<ChevronUp className={cn(s.icon, { [s.active]: display })} /> <ChevronRight className={cn(s.icon, { [s.active]: display })} />
</span> </span>
)} )}
</button> </button>

View File

@ -0,0 +1,19 @@
const System = ({ ...props }) => (
<svg
data-testid="geist-icon"
fill="none"
height="16"
shapeRendering="geometricPrecision"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
className="text-current"
{...props}
>
<path d="M2 13.381h20M8.66 19.05V22m6.84-2.95V22m-8.955 0h10.932M4 19.05h16a2 2 0 002-2V4a2 2 0 00-2-2H4a2 2 0 00-2 2v13.05a2 2 0 002 2z"></path>
</svg>
)
export default System

View File

@ -11,6 +11,7 @@ export { default as Cross } from './Cross'
export { default as Minus } from './Minus' export { default as Minus } from './Minus'
export { default as Check } from './Check' export { default as Check } from './Check'
export { default as Github } from './Github' export { default as Github } from './Github'
export { default as System } from './System'
export { default as Vercel } from './Vercel' export { default as Vercel } from './Vercel'
export { default as MapPin } from './MapPin' export { default as MapPin } from './MapPin'
export { default as ArrowLeft } from './ArrowLeft' export { default as ArrowLeft } from './ArrowLeft'

View File

@ -0,0 +1,22 @@
import { Moon, Sun, System } from '@components/icons'
interface ThemeIconProps {
theme?: string
width: number
height: number
}
const ThemeIcon = ({ theme, ...props }: ThemeIconProps) => {
switch (theme) {
case 'light':
return <Sun {...props} />
case 'dark':
return <Moon {...props} />
default:
return <System {...props} />
}
}
export default ThemeIcon

View File

@ -0,0 +1,80 @@
import { useState } from 'react'
import { ChevronRight, Cross } from '@components/icons'
import { useToggleTheme } from '@lib/hooks/useToggleTheme'
import cn from 'clsx'
import ClickOutside from '@lib/click-outside'
import ThemeIcon from './ThemeIcon'
const ThemeSwitcher = () => {
const [display, setDisplay] = useState(false)
const { theme, themes, setTheme } = useToggleTheme()
return (
<ClickOutside active={display} onClick={() => setDisplay(false)}>
<div className="relative">
<div
className="flex items-center relative"
onClick={() => setDisplay(!display)}
>
<button
className={
'w-[125px] h-10 pl-2 pr-1 rounded-md border border-accent-2 flex items-center justify-between transition-colors ease-linear hover:border-accent-3 hover:shadow-sm'
}
aria-label="Theme Switcher"
>
<span className="flex flex-shrink items-center">
<ThemeIcon width={20} height={20} theme={theme} />
<span className={cn('capitalize leading-none ml-2')}>
{theme}
</span>
</span>
<span className="cursor-pointer">
<ChevronRight
className={cn('transition duration-300', {
['rotate-90']: display,
})}
/>
</span>
</button>
</div>
<div className="absolute top-0 right-0">
{themes.length && display ? (
<div
className={
'fixed shadow-lg right-0 top-12 mt-2 origin-top-right w-full h-full outline-none bg-accent-0 z-40 lg:absolute lg:border lg:border-accent-1 lg:shadow-lg lg:w-56 lg:h-auto'
}
>
<div className="flex flex-row justify-end px-6">
<button
className="md:hidden"
onClick={() => setDisplay(false)}
aria-label="Close panel"
>
<Cross className="h-6 w-6" />
</button>
</div>
<ul>
{themes.map((t: string) => (
<li key={t}>
<button
className="flex w-full capitalize cursor-pointer px-6 py-3 transition ease-in-out duration-150 text-primary leading-6 font-medium items-center hover:bg-accent-1"
role={'link'}
onClick={() => {
setTheme(t)
setDisplay(false)
}}
>
{t}
</button>
</li>
))}
</ul>
</div>
) : null}
</div>
</div>
</ClickOutside>
)
}
export default ThemeSwitcher

View File

@ -0,0 +1 @@
export { default } from './ThemeSwitcher'

View File

@ -0,0 +1,11 @@
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
export const useToggleTheme = () => {
const { theme, themes, setTheme } = useTheme()
const [themeValue, setThemeValue] = useState<string>('system')
useEffect(() => setThemeValue(theme), [theme])
return { theme: themeValue, setTheme, themes }
}

View File

@ -34,7 +34,9 @@ export async function getStaticProps({
const { products: relatedProducts } = await allProductsPromise const { products: relatedProducts } = await allProductsPromise
if (!product) { if (!product) {
throw new Error(`Product with slug '${params!.slug}' not found`) return {
notFound: true,
}
} }
return { return {