diff --git a/README.md b/README.md index 0b752d8c6..2bb8b5525 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,11 @@ Demo live at: [demo.vercel.store](https://demo.vercel.store/) ## 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 -pnpm install & pnpm build # run this commands in root folder of the mono repo -pnpm dev # run this commands in the site folder +pnpm install & pnpm build # run these commands in the root folder of the mono repo +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 @@ -47,10 +47,10 @@ Next.js Commerce integrates out-of-the-box with BigCommerce, Shopify, Swell, Sal ## 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`). -- 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. -- 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. +- 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 the case of BigCommerce, the images CDN and additional API routes. ## Configuration @@ -73,7 +73,7 @@ Every provider defines the features that it supports under `packages/{provider}/ #### Features Available 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 - search @@ -83,7 +83,7 @@ For example: Turning `cart` off will disable Cart capabilities. #### 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` - You'll see a config file like this: diff --git a/packages/commerce/src/api/utils/errors.ts b/packages/commerce/src/api/utils/errors.ts index 5d54f8a75..227090c14 100644 --- a/packages/commerce/src/api/utils/errors.ts +++ b/packages/commerce/src/api/utils/errors.ts @@ -42,7 +42,9 @@ export class CommerceNetworkError extends Error { } 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) => { if (error instanceof ZodError) { diff --git a/packages/commerce/src/api/utils/with-operation-callback.ts b/packages/commerce/src/api/utils/with-operation-callback.ts index b4f5a88ee..1167cd5c2 100644 --- a/packages/commerce/src/api/utils/with-operation-callback.ts +++ b/packages/commerce/src/api/utils/with-operation-callback.ts @@ -21,7 +21,7 @@ export const withOperationCallback = const parse = ({ name, data }: Operation) => { switch (name) { case 'getProduct': - productSchema.nullable().parse(data.product) + productSchema.optional().parse(data.product) break case 'getAllProducts': z.array(productSchema).parse(data.products) diff --git a/packages/commercejs/src/api/operations/get-site-info.ts b/packages/commercejs/src/api/operations/get-site-info.ts index 3b768e611..f588b2bc8 100644 --- a/packages/commercejs/src/api/operations/get-site-info.ts +++ b/packages/commercejs/src/api/operations/get-site-info.ts @@ -27,7 +27,7 @@ export default function getSiteInfoOperation({ const { sdkFetch } = commerce.getConfig(config) const { data: categories } = await sdkFetch('categories', 'list') - const formattedCategories = categories.map(normalizeCategory) + const formattedCategories = categories?.map(normalizeCategory) ?? [] return { categories: formattedCategories, diff --git a/packages/commercejs/src/cart/use-add-item.tsx b/packages/commercejs/src/cart/use-add-item.tsx index b8fb8cb1c..0ad21bdc9 100644 --- a/packages/commercejs/src/cart/use-add-item.tsx +++ b/packages/commercejs/src/cart/use-add-item.tsx @@ -22,17 +22,17 @@ export const handler: MutationHook = { variables.push(item.variantId) } - const { cart } = await fetch<{ cart: CommercejsCart }>({ + const cart = await fetch({ query: options.query, method: options.method, variables, }) + return normalizeCart(cart) }, useHook: ({ fetch }) => function useHook() { const { mutate } = useCart() - return useCallback( async function addItem(input) { const cart = await fetch({ input }) diff --git a/packages/commercejs/src/cart/use-remove-item.tsx b/packages/commercejs/src/cart/use-remove-item.tsx index c06ac6d78..4923be5ab 100644 --- a/packages/commercejs/src/cart/use-remove-item.tsx +++ b/packages/commercejs/src/cart/use-remove-item.tsx @@ -16,7 +16,7 @@ export const handler: MutationHook = { method: 'remove', }, async fetcher({ input, options, fetch }) { - const { cart } = await fetch<{ cart: CommercejsCart }>({ + const cart = await fetch({ query: options.query, method: options.method, variables: input.itemId, diff --git a/packages/commercejs/src/cart/use-update-item.tsx b/packages/commercejs/src/cart/use-update-item.tsx index d9c38c60b..83faac35d 100644 --- a/packages/commercejs/src/cart/use-update-item.tsx +++ b/packages/commercejs/src/cart/use-update-item.tsx @@ -30,7 +30,7 @@ export const handler = { }, async fetcher({ input, options, fetch }: HookFetcherContext) { const variables = [input.itemId, { quantity: input.item.quantity }] - const { cart } = await fetch<{ cart: CommercejsCart }>({ + const cart = await fetch({ query: options.query, method: options.method, variables, @@ -57,7 +57,7 @@ export const handler = { const variantId = input.productId ?? item?.variantId const quantity = input?.quantity ?? item?.quantity - if (!itemId || !productId || !variantId) { + if (!itemId || !productId) { throw new ValidationError({ message: 'Invalid input for updating cart item', }) @@ -69,7 +69,7 @@ export const handler = { item: { quantity, productId, - variantId, + variantId: variantId ?? '', }, }, }) diff --git a/packages/commercejs/src/utils/normalize-cart.ts b/packages/commercejs/src/utils/normalize-cart.ts index 6bb7334be..909fa8335 100644 --- a/packages/commercejs/src/utils/normalize-cart.ts +++ b/packages/commercejs/src/utils/normalize-cart.ts @@ -44,14 +44,16 @@ const normalizeLineItem = ( } } -export const normalizeCart = (commercejsCart: CommercejsCart): Cart => { +export const normalizeCart = ( + commercejsCart: CommercejsCart | { cart: CommercejsCart } +): Cart => { const { id, created, subtotal: { raw: rawPrice }, currency, line_items, - } = commercejsCart + } = 'cart' in commercejsCart ? commercejsCart.cart : commercejsCart return { id, diff --git a/packages/commercejs/src/utils/normalize-product.ts b/packages/commercejs/src/utils/normalize-product.ts index 2bd45921f..97f5d4027 100644 --- a/packages/commercejs/src/utils/normalize-product.ts +++ b/packages/commercejs/src/utils/normalize-product.ts @@ -54,6 +54,7 @@ export function normalizeProduct( ): Product { const { id, name, description, permalink, assets, price, variant_groups } = commercejsProduct + return { id, name, @@ -61,15 +62,19 @@ export function normalizeProduct( descriptionHtml: description, slug: permalink, path: `/${permalink}`, - images: assets.map(({ url, description, filename }) => ({ - url, - alt: description || filename, - })), + images: + assets?.map(({ url, description, filename }) => ({ + url, + alt: description || filename, + })) || [], price: { value: price.raw, currencyCode: 'USD', }, - variants: normalizeVariants(commercejsProductVariants, variant_groups), - options: getOptionsFromVariantGroups(variant_groups), + variants: normalizeVariants( + commercejsProductVariants, + variant_groups || [] + ), + options: variant_groups ? getOptionsFromVariantGroups(variant_groups) : [], } } diff --git a/packages/saleor/src/api/utils/fetch-graphql-api.ts b/packages/saleor/src/api/utils/fetch-graphql-api.ts index 313b57a05..45f5326d5 100644 --- a/packages/saleor/src/api/utils/fetch-graphql-api.ts +++ b/packages/saleor/src/api/utils/fetch-graphql-api.ts @@ -2,7 +2,6 @@ import type { GraphQLFetcher } from '@vercel/commerce/api' import { API_URL } from '../../const' import { getError } from '../../utils/handle-fetch-response' -import { getCommerceApi } from '..' import { getToken } from '../../utils/index' const fetchGraphqlApi: GraphQLFetcher = async ( @@ -10,7 +9,6 @@ const fetchGraphqlApi: GraphQLFetcher = async ( { variables } = {}, headers?: HeadersInit ) => { - const config = getCommerceApi().getConfig() const token = getToken() 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) { - throw getError(errors, status) + if (errors || res.status >= 400) { + throw getError( + errors || [ + { + message: `${type ? `${type}, ` : ''}${message}`, + }, + ], + status || res.status + ) } return { data, res } diff --git a/site/components/common/Footer/Footer.tsx b/site/components/common/Footer/Footer.tsx index ebf3cce4a..8a7936fdf 100644 --- a/site/components/common/Footer/Footer.tsx +++ b/site/components/common/Footer/Footer.tsx @@ -7,6 +7,7 @@ import getSlug from '@lib/get-slug' import { Github, Vercel } from '@components/icons' import { Logo, Container } from '@components/ui' import { I18nWidget } from '@components/common' +import ThemeSwitcher from '@components/ui/ThemeSwitcher' import s from './Footer.module.css' interface Props { @@ -41,7 +42,7 @@ const Footer: FC = ({ className, pages }) => { ACME -
+
{[...links, ...sitePages].map((page) => ( @@ -55,8 +56,10 @@ const Footer: FC = ({ className, pages }) => { ))}
-
- diff --git a/site/components/common/I18nWidget/I18nWidget.module.css b/site/components/common/I18nWidget/I18nWidget.module.css index 5b486d537..a3c19babb 100644 --- a/site/components/common/I18nWidget/I18nWidget.module.css +++ b/site/components/common/I18nWidget/I18nWidget.module.css @@ -3,7 +3,7 @@ } .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 { @@ -32,7 +32,7 @@ } .icon.active { - transform: rotate(180deg); + transform: rotate(90deg); } @screen lg { diff --git a/site/components/common/I18nWidget/I18nWidget.tsx b/site/components/common/I18nWidget/I18nWidget.tsx index 11d0c8f1a..013c68f9a 100644 --- a/site/components/common/I18nWidget/I18nWidget.tsx +++ b/site/components/common/I18nWidget/I18nWidget.tsx @@ -3,7 +3,7 @@ import Link from 'next/link' import { FC, useState } from 'react' import { useRouter } from 'next/router' 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 Image from 'next/image' interface LOCALE_DATA { @@ -54,14 +54,14 @@ const I18nWidget: FC = () => { {LOCALES_MAP[currentLocale].img.alt} {options && ( - - + + )} diff --git a/site/components/icons/System.tsx b/site/components/icons/System.tsx new file mode 100644 index 000000000..bb7367c7a --- /dev/null +++ b/site/components/icons/System.tsx @@ -0,0 +1,19 @@ +const System = ({ ...props }) => ( + + + +) + +export default System diff --git a/site/components/icons/index.ts b/site/components/icons/index.ts index 12e0cc202..72a65494c 100644 --- a/site/components/icons/index.ts +++ b/site/components/icons/index.ts @@ -11,6 +11,7 @@ export { default as Cross } from './Cross' export { default as Minus } from './Minus' export { default as Check } from './Check' export { default as Github } from './Github' +export { default as System } from './System' export { default as Vercel } from './Vercel' export { default as MapPin } from './MapPin' export { default as ArrowLeft } from './ArrowLeft' diff --git a/site/components/ui/ThemeSwitcher/ThemeIcon.tsx b/site/components/ui/ThemeSwitcher/ThemeIcon.tsx new file mode 100644 index 000000000..c65442a74 --- /dev/null +++ b/site/components/ui/ThemeSwitcher/ThemeIcon.tsx @@ -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 + + case 'dark': + return + + default: + return + } +} + +export default ThemeIcon diff --git a/site/components/ui/ThemeSwitcher/ThemeSwitcher.tsx b/site/components/ui/ThemeSwitcher/ThemeSwitcher.tsx new file mode 100644 index 000000000..424410511 --- /dev/null +++ b/site/components/ui/ThemeSwitcher/ThemeSwitcher.tsx @@ -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 ( + setDisplay(false)}> +
+
setDisplay(!display)} + > + +
+
+ {themes.length && display ? ( +
+
+ +
+
    + {themes.map((t: string) => ( +
  • + +
  • + ))} +
+
+ ) : null} +
+
+
+ ) +} + +export default ThemeSwitcher diff --git a/site/components/ui/ThemeSwitcher/index.ts b/site/components/ui/ThemeSwitcher/index.ts new file mode 100644 index 000000000..2b4931135 --- /dev/null +++ b/site/components/ui/ThemeSwitcher/index.ts @@ -0,0 +1 @@ +export { default } from './ThemeSwitcher' diff --git a/site/lib/hooks/useToggleTheme.ts b/site/lib/hooks/useToggleTheme.ts new file mode 100644 index 000000000..563af6533 --- /dev/null +++ b/site/lib/hooks/useToggleTheme.ts @@ -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('system') + + useEffect(() => setThemeValue(theme), [theme]) + + return { theme: themeValue, setTheme, themes } +} diff --git a/site/pages/product/[slug].tsx b/site/pages/product/[slug].tsx index 9d8d153e4..b49aa6398 100644 --- a/site/pages/product/[slug].tsx +++ b/site/pages/product/[slug].tsx @@ -34,7 +34,9 @@ export async function getStaticProps({ const { products: relatedProducts } = await allProductsPromise if (!product) { - throw new Error(`Product with slug '${params!.slug}' not found`) + return { + notFound: true, + } } return {