diff --git a/.env.template b/.env.template index 73a8a6e3b..9b45afe4b 100644 --- a/.env.template +++ b/.env.template @@ -2,4 +2,8 @@ BIGCOMMERCE_STOREFRONT_API_URL= BIGCOMMERCE_STOREFRONT_API_TOKEN= BIGCOMMERCE_STORE_API_URL= BIGCOMMERCE_STORE_API_TOKEN= -BIGCOMMERCE_STORE_API_CLIENT_ID= \ No newline at end of file +BIGCOMMERCE_STORE_API_CLIENT_ID= +BIGCOMMERCE_CHANNEL_ID= + +SHOPIFY_STORE_DOMAIN= +SHOPIFY_STOREFRONT_ACCESS_TOKEN= diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..e1076edfa --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "useTabs": false +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..c83e26348 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["esbenp.prettier-vscode"] +} diff --git a/README.md b/README.md index 8eb69383c..885c95e85 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ Start right now at [nextjs.org/commerce](https://nextjs.org/commerce) Demo live at: [demo.vercel.store](https://demo.vercel.store/) -This project is currently under development. +- Shopify Demo: https://shopify.demo.vercel.store/ +- BigCommerce Demo: https://bigcommerce.demo.vercel.store/ ## Features @@ -21,26 +22,122 @@ This project is currently under development. - Integrations - Integrate seamlessly with the most common ecommerce platforms. - Dark Mode Support +## Integrations + +Next.js Commerce integrates out-of-the-box with BigCommerce and Shopify. We plan to support all major ecommerce backends. + +## Considerations + +- `framework/commerce` contains all types, helpers and functions to be used as base to build a new **provider**. +- **Providers** live under `framework`'s root folder and they will extend Next.js Commerce types and functionality. +- **Features API** is 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. +- **Providers don't depend on anything that's specific to the application they're used in**. They only depend on `framework/commerce`, on their own framework folder and on some dependencies included in `package.json` +- We recommend that each **provider** ships with an `env.template` file and a `[readme.md](http://readme.md)` file. + +## Provider Structure + +Next.js Commerce provides a set of utilities and functions to create new providers. This is how a provider structure looks like. + +- `product` + - usePrice + - useSearch + - getProduct + - getAllProducts +- `wishlist` + - useWishlist + - useAddItem + - useRemoveItem +- `auth` + - useLogin + - useLogout + - useSignup +- `customer` + - useCustomer + - getCustomerId + - getCustomerWistlist +- `cart` + - useCart + - useAddItem + - useRemoveItem + - useUpdateItem +- `env.template` +- `provider.ts` +- `commerce.config.json` +- `next.config.js` +- `README.md` + +## Configuration + +### How to change providers + +First, update the provider selected in `commerce.config.json`: + +```json +{ + "provider": "bigcommerce", + "features": { + "wishlist": true + } +} +``` + +Then, change the paths defined in `tsconfig.json` and update the `@framework` paths to point to the right folder provider: + +```json +"@framework": ["framework/bigcommerce"], +"@framework/*": ["framework/bigcommerce/*"] +``` + +Make sure to add the environment variables required by the new provider. + +### Features + +Every provider defines the features that it supports under `framework/{provider}/commerce.config.json` + +#### 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) + +- Open `commerce.config.json` +- You'll see a config file like this: + ```json + { + "provider": "bigcommerce", + "features": { + "wishlist": false + } + } + ``` +- Turn wishlist on by setting wishlist to true. +- Run the app and the wishlist functionality should be back on. + +### How to create a new provider + +We'd recommend to duplicate a provider folder and push your providers SDK. + +If you succeeded building a provider, submit a PR so we can all enjoy it. + ## Work in progress + We're using Github Projects to keep track of issues in progress and todo's. Here is our [Board](https://github.com/vercel/commerce/projects/1) -## Integrations -Next.js Commerce integrates out-of-the-box with BigCommerce. We plan to support all major ecommerce backends. - - -## Goals - -* **Next.js Commerce** should have a completely data **agnostic** UI -* **Aware of schema**: should ship with the right data schemas and types. -* All providers should return the right data types and schemas to blend correctly with Next.js Commerce. -* `@framework` will be the alias utilized in commerce and it will map to the ecommerce provider of preference- e.g BigCommerce, Shopify, Swell. All providers should expose the same standardized functions. _Note that the same applies for recipes using a CMS + an ecommerce provider._ - -There is a `framework` folder in the root folder that will contain multiple ecommerce providers. - -Additionally, we need to ensure feature parity (not all providers have e.g. wishlist) we will also have to build a feature API to disable/enable features in the UI. - People actively working on this project: @okbel & @lfades. +## Contribute + +Our commitment to Open Source can be found [here](https://vercel.com/oss). + +1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device. +2. Create a new branch `git checkout -b MY_BRANCH_NAME` +3. Install yarn: `npm install -g yarn` +4. Install the dependencies: `yarn` +5. Duplicate `.env.template` and rename it to `.env.local`. +6. Add proper store values to `.env.local`. +7. Run `yarn dev` to build and watch for code changes +8. The development branch is `canary` (this is the branch pull requests should be made against). + On a release, `canary` branch is rebased into `master`. + ## Troubleshoot
@@ -57,6 +154,7 @@ BIGCOMMERCE_STOREFRONT_API_TOKEN=<> BIGCOMMERCE_STORE_API_URL=<> BIGCOMMERCE_STORE_API_TOKEN=<> BIGCOMMERCE_STORE_API_CLIENT_ID=<> +BIGCOMMERCE_CHANNEL_ID=<> ``` If your project was started with a "Deploy with Vercel" button, you can use Vercel's CLI to retrieve these credentials. @@ -77,22 +175,3 @@ After Email confirmation, Checkout should be manually enabled through BigCommerc
BigCommerce team has been notified and they plan to add more detailed about this subject.
- -## Contribute - -Our commitment to Open Source can be found [here](https://vercel.com/oss). - -1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device. -2. Create a new branch `git checkout -b MY_BRANCH_NAME` -3. Install yarn: `npm install -g yarn` -4. Install the dependencies: `yarn` -5. Duplicate `.env.template` and rename it to `.env.local`. -6. Add proper store values to `.env.local`. -7. Run `yarn dev` to build and watch for code changes -8. The development branch is `canary` (this is the branch pull requests should be made against). - On a release, `canary` branch is rebased into `master`. - - - - - diff --git a/commerce.config.json b/commerce.config.json new file mode 100644 index 000000000..bef7db222 --- /dev/null +++ b/commerce.config.json @@ -0,0 +1,7 @@ +{ + "provider": "bigcommerce", + "features": { + "wishlist": true, + "customCheckout": false + } +} diff --git a/components/auth/LoginView.tsx b/components/auth/LoginView.tsx index 9102a53c6..89d5bf893 100644 --- a/components/auth/LoginView.tsx +++ b/components/auth/LoginView.tsx @@ -1,6 +1,6 @@ import { FC, useEffect, useState, useCallback } from 'react' import { Logo, Button, Input } from '@components/ui' -import useLogin from '@framework/use-login' +import useLogin from '@framework/auth/use-login' import { useUI } from '@components/ui/context' import { validate } from 'email-validator' diff --git a/components/auth/SignUpView.tsx b/components/auth/SignUpView.tsx index c49637d47..1b619828b 100644 --- a/components/auth/SignUpView.tsx +++ b/components/auth/SignUpView.tsx @@ -3,7 +3,7 @@ import { validate } from 'email-validator' import { Info } from '@components/icons' import { useUI } from '@components/ui/context' import { Logo, Button, Input } from '@components/ui' -import useSignup from '@framework/use-signup' +import useSignup from '@framework/auth/use-signup' interface Props {} diff --git a/components/cart/CartItem/CartItem.tsx b/components/cart/CartItem/CartItem.tsx index 2769ac715..cb7f8600e 100644 --- a/components/cart/CartItem/CartItem.tsx +++ b/components/cart/CartItem/CartItem.tsx @@ -2,43 +2,51 @@ import { ChangeEvent, useEffect, useState } from 'react' import cn from 'classnames' import Image from 'next/image' import Link from 'next/link' +import s from './CartItem.module.css' import { Trash, Plus, Minus } from '@components/icons' -import usePrice from '@framework/use-price' +import { useUI } from '@components/ui/context' +import type { LineItem } from '@framework/types' +import usePrice from '@framework/product/use-price' import useUpdateItem from '@framework/cart/use-update-item' import useRemoveItem from '@framework/cart/use-remove-item' -import s from './CartItem.module.css' type ItemOption = { - name: string, - nameId: number, - value: string, + name: string + nameId: number + value: string valueId: number } const CartItem = ({ item, currencyCode, + ...rest }: { - item: any + item: LineItem currencyCode: string }) => { + const { closeSidebarIfPresent } = useUI() + const { price } = usePrice({ - amount: item.extended_sale_price, - baseAmount: item.extended_list_price, + amount: item.variant.price * item.quantity, + baseAmount: item.variant.listPrice * item.quantity, currencyCode, }) - const updateItem = useUpdateItem(item) + + const updateItem = useUpdateItem({ item }) const removeItem = useRemoveItem() const [quantity, setQuantity] = useState(item.quantity) const [removing, setRemoving] = useState(false) + const updateQuantity = async (val: number) => { await updateItem({ quantity: val }) } + const handleQuantity = (e: ChangeEvent) => { const val = Number(e.target.value) if (Number.isInteger(val) && val >= 0) { - setQuantity(e.target.value) + setQuantity(Number(e.target.value)) } } const handleBlur = () => { @@ -62,11 +70,13 @@ const CartItem = ({ try { // If this action succeeds then there's no need to do `setRemoving(true)` // because the component will be removed from the view - await removeItem({ id: item.id }) + await removeItem(item) } catch (error) { setRemoving(false) } } + // TODO: Add a type for this + const options = (item as any).options useEffect(() => { // Reset the quantity state if the item quantity changes @@ -80,32 +90,38 @@ const CartItem = ({ className={cn('flex flex-row space-x-8 py-8', { 'opacity-75 pointer-events-none': removing, })} + {...rest} >
Product Image
- {/** TODO: Replace this. No `path` found at Cart */} - - + + closeSidebarIfPresent()} + > {item.name} - {item.options && item.options.length > 0 ? ( + {options && options.length > 0 ? (
- {item.options.map((option:ItemOption, i: number) => - - {option.value}{ i === item.options.length -1 ? "" : ", " } + {options.map((option: ItemOption, i: number) => ( + + {option.value} + {i === options.length - 1 ? '' : ', '} - )} + ))}
) : null}
@@ -130,7 +146,10 @@ const CartItem = ({
{price} -
diff --git a/components/cart/CartSidebarView/CartSidebarView.tsx b/components/cart/CartSidebarView/CartSidebarView.tsx index eb76c5da5..326390327 100644 --- a/components/cart/CartSidebarView/CartSidebarView.tsx +++ b/components/cart/CartSidebarView/CartSidebarView.tsx @@ -1,42 +1,40 @@ import { FC } from 'react' import cn from 'classnames' -import { UserNav } from '@components/common' -import { Button } from '@components/ui' -import { Bag, Cross, Check } from '@components/icons' -import { useUI } from '@components/ui/context' -import useCart from '@framework/cart/use-cart' -import usePrice from '@framework/use-price' +import Link from 'next/link' import CartItem from '../CartItem' import s from './CartSidebarView.module.css' +import { Button } from '@components/ui' +import { UserNav } from '@components/common' +import { useUI } from '@components/ui/context' +import { Bag, Cross, Check } from '@components/icons' +import useCart from '@framework/cart/use-cart' +import usePrice from '@framework/product/use-price' const CartSidebarView: FC = () => { const { closeSidebar } = useUI() - const { data, isEmpty } = useCart() + const { data, isLoading, isEmpty } = useCart() + const { price: subTotal } = usePrice( data && { - amount: data.base_amount, + amount: Number(data.subtotalPrice), currencyCode: data.currency.code, } ) const { price: total } = usePrice( data && { - amount: data.cart_amount, + amount: Number(data.totalPrice), currencyCode: data.currency.code, } ) const handleClose = () => closeSidebar() - const items = data?.line_items.physical_items ?? [] - const error = null const success = null return (
@@ -51,12 +49,12 @@ const CartSidebarView: FC = () => {
- +
- {isEmpty ? ( + {isLoading || isEmpty ? (
@@ -90,15 +88,20 @@ const CartSidebarView: FC = () => { ) : ( <>
-

- My Cart -

+ +

+ My Cart +

+
    - {items.map((item: any) => ( + {data!.lineItems.map((item: any) => ( ))}
diff --git a/components/common/Footer/Footer.tsx b/components/common/Footer/Footer.tsx index c90b4886f..75b2806ef 100644 --- a/components/common/Footer/Footer.tsx +++ b/components/common/Footer/Footer.tsx @@ -2,7 +2,7 @@ import { FC } from 'react' import cn from 'classnames' import Link from 'next/link' import { useRouter } from 'next/router' -import type { Page } from '@framework/api/operations/get-all-pages' +import type { Page } from '@framework/common/get-all-pages' import getSlug from '@lib/get-slug' import { Github, Vercel } from '@components/icons' import { Logo, Container } from '@components/ui' diff --git a/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx b/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx index f19e1586a..423048f75 100644 --- a/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx +++ b/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx @@ -1,5 +1,6 @@ import { FC } from 'react' import Link from 'next/link' +import type { Product } from '@commerce/types' import { Grid } from '@components/ui' import { ProductCard } from '@components/product' import s from './HomeAllProductsGrid.module.css' @@ -8,10 +9,14 @@ import { getCategoryPath, getDesignerPath } from '@lib/search' interface Props { categories?: any brands?: any - newestProducts?: any + products?: Product[] } -const Head: FC = ({ categories, brands, newestProducts }) => { +const HomeAllProductsGrid: FC = ({ + categories, + brands, + products = [], +}) => { return (
@@ -48,13 +53,15 @@ const Head: FC = ({ categories, brands, newestProducts }) => {
- {newestProducts.map(({ node }: any) => ( + {products.map((product) => ( ))} @@ -63,4 +70,4 @@ const Head: FC = ({ categories, brands, newestProducts }) => { ) } -export default Head +export default HomeAllProductsGrid diff --git a/components/common/I18nWidget/I18nWidget.tsx b/components/common/I18nWidget/I18nWidget.tsx index 9bfc7b63c..fbe67a4c1 100644 --- a/components/common/I18nWidget/I18nWidget.tsx +++ b/components/common/I18nWidget/I18nWidget.tsx @@ -43,7 +43,7 @@ const I18nWidget: FC = () => { const currentLocale = locale || defaultLocale return ( - setDisplay(false)} > + setDisplay(false)}>
-
- +
+
- -
- ) -} + +
+ +
+
+ +
+ +
+ + +) export default Navbar diff --git a/components/common/Navbar/NavbarRoot.tsx b/components/common/Navbar/NavbarRoot.tsx new file mode 100644 index 000000000..2eb8c5429 --- /dev/null +++ b/components/common/Navbar/NavbarRoot.tsx @@ -0,0 +1,33 @@ +import { FC, useState, useEffect } from 'react' +import throttle from 'lodash.throttle' +import cn from 'classnames' +import s from './Navbar.module.css' + +const NavbarRoot: FC = ({ children }) => { + const [hasScrolled, setHasScrolled] = useState(false) + + useEffect(() => { + const handleScroll = throttle(() => { + const offset = 0 + const { scrollTop } = document.documentElement + const scrolled = scrollTop > offset + + if (hasScrolled !== scrolled) { + setHasScrolled(scrolled) + } + }, 200) + + document.addEventListener('scroll', handleScroll) + return () => { + document.removeEventListener('scroll', handleScroll) + } + }, [hasScrolled]) + + return ( +
+ {children} +
+ ) +} + +export default NavbarRoot diff --git a/components/common/UserNav/DropdownMenu.tsx b/components/common/UserNav/DropdownMenu.tsx index 7b02c863a..43f842009 100644 --- a/components/common/UserNav/DropdownMenu.tsx +++ b/components/common/UserNav/DropdownMenu.tsx @@ -8,6 +8,7 @@ import { Avatar } from '@components/common' import { Moon, Sun } from '@components/icons' import { useUI } from '@components/ui/context' import ClickOutside from '@lib/click-outside' +import useLogout from '@framework/auth/use-logout' import { disableBodyScroll, @@ -15,8 +16,6 @@ import { clearAllBodyScrollLocks, } from 'body-scroll-lock' -import useLogout from '@framework/use-logout' - interface DropdownMenuProps { open?: boolean } diff --git a/components/common/UserNav/UserNav.tsx b/components/common/UserNav/UserNav.tsx index 31852f658..4d00970a9 100644 --- a/components/common/UserNav/UserNav.tsx +++ b/components/common/UserNav/UserNav.tsx @@ -1,27 +1,26 @@ import { FC } from 'react' import Link from 'next/link' import cn from 'classnames' +import type { LineItem } from '@framework/types' import useCart from '@framework/cart/use-cart' -import useCustomer from '@framework/use-customer' +import useCustomer from '@framework/customer/use-customer' +import { Avatar } from '@components/common' import { Heart, Bag } from '@components/icons' import { useUI } from '@components/ui/context' import DropdownMenu from './DropdownMenu' import s from './UserNav.module.css' -import { Avatar } from '@components/common' interface Props { className?: string } -const countItem = (count: number, item: any) => count + item.quantity -const countItems = (count: number, items: any[]) => - items.reduce(countItem, count) +const countItem = (count: number, item: LineItem) => count + item.quantity -const UserNav: FC = ({ className, children, ...props }) => { +const UserNav: FC = ({ className }) => { const { data } = useCart() const { data: customer } = useCustomer() const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI() - const itemsCount = Object.values(data?.line_items ?? {}).reduce(countItems, 0) + const itemsCount = data?.lineItems.reduce(countItem, 0) ?? 0 return (