diff --git a/.env.template b/.env.template index 73a8a6e3b..b68daaffe 100644 --- a/.env.template +++ b/.env.template @@ -1,5 +1,15 @@ +# Available providers: bigcommerce, shopify, swell +COMMERCE_PROVIDER= + 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= + +NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN= +NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN= + +NEXT_PUBLIC_SWELL_STORE_ID= +NEXT_PUBLIC_SWELL_PUBLIC_KEY= diff --git a/.gitignore b/.gitignore index 50d4285ba..22f1bf4f3 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ out/ # misc .DS_Store *.pem +.idea # debug npm-debug.log* 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/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 7d1d95638..000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,4 +0,0 @@ -## Changelog - -- Select Variants Working -- Click on cart item title, closes the sidebar diff --git a/README.md b/README.md index ea6248c51..8f29734e5 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,10 @@ 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.vercel.store/ +- Swell Demo: https://swell.vercel.store/ +- BigCommerce Demo: https://bigcommerce.vercel.store/ +- Vendure Demo: https://vendure.vercel.store ## Features @@ -21,82 +24,66 @@ This project is currently under development. - Integrations - Integrate seamlessly with the most common ecommerce platforms. - Dark Mode Support -## 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. +Next.js Commerce integrates out-of-the-box with BigCommerce and Shopify. We plan to support all major ecommerce backends. -## Goals +## Considerations -- **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._ +- `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 (`framework/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. +- **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` -There is a `framework` folder in the root folder that will contain multiple ecommerce providers. +## Configuration -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. +### How to change providers -People actively working on this project: @okbel & @lfades. +Open `.env.local` and change the value of `COMMERCE_PROVIDER` to the provider you would like to use, then set the environment variables for that provider (use `.env.template` as the base). -## Framework +The setup for Shopify would look like this for example: -Framework is where the data comes from. It contains mostly hooks and functions. - -## Structure - -Main folder and its exposed functions - -- `product` - - usePrice - - useSearch - - getProduct - - getAllProducts -- `wishlist` - - useWishlist - - addWishlistItem - - removeWishlistItem -- `auth` - - useLogin - - useLogout - - useSignup -- `cart` - - - useCart - - useAddItem - - useRemoveItem - - useCartActions - - useUpdateItem - -- `config.json` -- README.md - -#### Example of correct usage of Commece Framework - -```js -import { useUI } from '@components/ui' -import { useCustomer } from '@framework/customer' -import { useAddItem, useWishlist, useRemoveItem } from '@framework/wishlist' +``` +COMMERCE_PROVIDER=shopify +NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxx +NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=xxxxxxx.myshopify.com ``` -## Config +And check that the `tsconfig.json` resolves to the chosen provider: + +``` + "@framework": ["framework/shopify"], + "@framework/*": ["framework/shopify/*"] +``` + +That's it! ### Features -In order to make the UI entirely functional, we need to specify which features certain providers do not **provide**. +Every provider defines the features that it supports under `framework/{provider}/commerce.config.json` -**Disabling wishlist:** +#### How to turn Features on and off -``` -{ - "features": { - "wishlist": false +> 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 + { + "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 + +Follow our docs for [Adding a new Commerce Provider](framework/commerce/new-provider.md). + +If you succeeded building a provider, submit a PR with a valid demo and we'll review it asap. ## Contribute @@ -106,11 +93,15 @@ Our commitment to Open Source can be found [here](https://vercel.com/oss). 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`. +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`. + +## 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) + +People actively working on this project: @okbel & @lfades. ## Troubleshoot @@ -128,6 +119,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. diff --git a/framework/commerce/config.json b/commerce.config.json similarity index 100% rename from framework/commerce/config.json rename to commerce.config.json diff --git a/components/cart/CartItem/CartItem.tsx b/components/cart/CartItem/CartItem.tsx index bb57c3f25..e6820d32c 100644 --- a/components/cart/CartItem/CartItem.tsx +++ b/components/cart/CartItem/CartItem.tsx @@ -33,7 +33,7 @@ const CartItem = ({ currencyCode, }) - const updateItem = useUpdateItem(item) + const updateItem = useUpdateItem({ item }) const removeItem = useRemoveItem() const [quantity, setQuantity] = useState(item.quantity) const [removing, setRemoving] = useState(false) @@ -92,15 +92,18 @@ const CartItem = ({ })} {...rest} > -
{description}
- + Read it hereLoading...
+ if (error) return{error.message}
+ if (!data) return null + + returnLoading...
+ if (error) return{error.message}
+ if (isEmpty) returnThe cart is empty
+ + returnThe cart total is {data.totalPrice}
+} +``` + +### useAddItem + +Returns a function that adds a new item to the cart when called, if this is the first item it will create the cart: + +```jsx +import { useAddItem } from '@framework/cart' + +const AddToCartButton = ({ productId, variantId }) => { + const addItem = useAddItem() + + const addToCart = async () => { + await addItem({ + productId, + variantId, + }) + } + + return +} +``` + +### useUpdateItem + +Returns a function that updates a current item in the cart when called, usually the quantity. + +```jsx +import { useUpdateItem } from '@framework/cart' + +const CartItemQuantity = ({ item }) => { + const [quantity, setQuantity] = useState(item.quantity) + const updateItem = useUpdateItem({ item }) + + const updateQuantity = async (e) => { + const val = e.target.value + + setQuantity(val) + await updateItem({ quantity: val }) + } + + return ( + + ) +} +``` + +If the `quantity` is lower than 1 the item will be removed from the cart. + +### useRemoveItem + +Returns a function that removes an item in the cart when called: + +```jsx +import { useRemoveItem } from '@framework/cart' + +const RemoveButton = ({ item }) => { + const removeItem = useRemoveItem() + const handleRemove = async () => { + await removeItem(item) + } + + return +} +``` + +## Wishlist Hooks + +Wishlist hooks work just like [cart hooks](#cart-hooks). Feel free to check how those work first. + +The example below shows how to use the `useWishlist`, `useAddItem` and `useRemoveItem` hooks: + +```jsx +import { useWishlist, useAddItem, useRemoveItem } from '@framework/wishlist' + +const WishlistButton = ({ productId, variant }) => { + const addItem = useAddItem() + const removeItem = useRemoveItem() + const { data, isLoading, isEmpty, error } = useWishlist() + + if (isLoading) returnLoading...
+ if (error) return{error.message}
+ if (isEmpty) returnThe wihslist is empty
+ + const { data: customer } = useCustomer() + const itemInWishlist = data?.items?.find( + (item) => item.product_id === productId && item.variant_id === variant.id + ) + + const handleWishlistChange = async (e) => { + e.preventDefault() + if (!customer) return + + if (itemInWishlist) { + await removeItem({ id: itemInWishlist.id }) + } else { + await addItem({ + productId, + variantId: variant.id, + }) + } + } + + return ( + + ) +} +``` + +## Commerce API + +While commerce hooks focus on client side data fetching and interactions, the commerce API focuses on static content generation for pages and API endpoints in a Node.js context. + +> The commerce API is currently going through a refactor in https://github.com/vercel/commerce/pull/252 - We'll update the docs once the API is released. + +## More + +Feel free to read through the source for more usage, and check the commerce vercel demo and commerce repo for usage examples: ([demo.vercel.store](https://demo.vercel.store/)) ([repo](https://github.com/vercel/commerce)) diff --git a/framework/commerce/api/index.ts b/framework/commerce/api/index.ts index 77b2eeb7e..c15ca7a79 100644 --- a/framework/commerce/api/index.ts +++ b/framework/commerce/api/index.ts @@ -2,6 +2,7 @@ import type { RequestInit, Response } from '@vercel/fetch' export interface CommerceAPIConfig { locale?: string + locales?: string[] commerceUrl: string apiToken: string cartCookie: string diff --git a/framework/commerce/auth/use-login.tsx b/framework/commerce/auth/use-login.tsx new file mode 100644 index 000000000..cc4cf6a73 --- /dev/null +++ b/framework/commerce/auth/use-login.tsx @@ -0,0 +1,19 @@ +import { useHook, useMutationHook } from '../utils/use-hook' +import { mutationFetcher } from '../utils/default-fetcher' +import type { MutationHook, HookFetcherFn } from '../utils/types' +import type { Provider } from '..' + +export type UseLogin< + H extends MutationHook= Prop< - Prop
, - 'useAddItem' -> - -// Input expected by the action returned by the `useAddItem` hook -export type UseAddItemInput
= UseHookInput< - UseAddItemHandler
-> - -export type UseAddItemResult
= ReturnType<
- UseHookResponse = Partial<
- UseAddItemInput
-> extends UseAddItemInput
- ? (input?: UseAddItemInput ) => (input: Input) => UseAddItemResult
- : (input: UseAddItemInput ) => (input: Input) => UseAddItemResult
+export type UseAddItem<
+ H extends MutationHook (
- input: UseAddItemInput
-) {
- const { providerRef, fetcherRef } = useCommerce ()
-
- const provider = providerRef.current
- const opts = provider.cart?.useAddItem
-
- const fetcherFn = opts?.fetcher ?? fetcher
- const useHook = opts?.useHook ?? (() => () => {})
- const fetchFn = provider.fetcher ?? fetcherRef.current
- const action = useHook({ input })
-
- return useCallback(
- function addItem(input: Input) {
- return action({
- input,
- fetch({ input }) {
- return fetcherFn({
- input,
- options: opts!.fetchOptions,
- fetch: fetchFn,
- })
- },
- })
- },
- [input, fetchFn, opts?.fetchOptions]
- )
-}
+export default useAddItem
diff --git a/framework/commerce/cart/use-cart-actions.tsx b/framework/commerce/cart/use-cart-actions.tsx
deleted file mode 100644
index 3ba4b2e1a..000000000
--- a/framework/commerce/cart/use-cart-actions.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { HookFetcher, HookFetcherOptions } from '../utils/types'
-import useAddItem from './use-add-item'
-import useRemoveItem from './use-remove-item'
-import useUpdateItem from './use-update-item'
-
-// This hook is probably not going to be used, but it's here
-// to show how a commerce should be structuring it
-export default function useCartActions = Prop<
- Prop ,
- 'useCart'
->
-
-export type UseCartInput = UseHookInput = UseHookResponse<
- UseCartHandler
->
-
-export type UseCart = Partial<
- UseCartInput
-> extends UseCartInput
- ? (input?: UseCartInput ) => CartResponse
- : (input: UseCartInput ) => CartResponse
+export type UseCart<
+ H extends SWRHook (
- input: UseCartInput = {}
-) {
- const { providerRef, fetcherRef, cartCookie } = useCommerce ()
-
- const provider = providerRef.current
- const opts = provider.cart?.useCart
-
- const fetcherFn = opts?.fetcher ?? fetcher
- const useHook = opts?.useHook ?? ((ctx) => ctx.useData())
+const fn = (provider: Provider) => provider.cart?.useCart!
+const useCart: UseCart = (input) => {
+ const hook = useHook(fn)
+ const { cartCookie } = useCommerce()
+ const fetcherFn = hook.fetcher ?? fetcher
const wrapper: typeof fetcher = (context) => {
context.input.cartId = Cookies.get(cartCookie)
return fetcherFn(context)
}
-
- return useHook({
- input,
- useData(ctx) {
- const response = useData(
- { ...opts!, fetcher: wrapper },
- ctx?.input ?? [],
- provider.fetcher ?? fetcherRef.current,
- ctx?.swrOptions ?? input.swrOptions
- )
- return response
- },
- })
+ return useSWRHook({ ...hook, fetcher: wrapper })(input)
}
+
+export default useCart
diff --git a/framework/commerce/cart/use-remove-item.tsx b/framework/commerce/cart/use-remove-item.tsx
index 8a63b1b73..a9d1b37d2 100644
--- a/framework/commerce/cart/use-remove-item.tsx
+++ b/framework/commerce/cart/use-remove-item.tsx
@@ -1,10 +1,35 @@
-import useAction from '../utils/use-action'
+import { useHook, useMutationHook } from '../utils/use-hook'
+import { mutationFetcher } from '../utils/default-fetcher'
+import type { HookFetcherFn, MutationHook } from '../utils/types'
+import type { Cart, LineItem, RemoveCartItemBody } from '../types'
+import type { Provider } from '..'
-// Input expected by the action returned by the `useRemoveItem` hook
-export interface RemoveItemInput {
+/**
+ * Input expected by the action returned by the `useRemoveItem` hook
+ */
+export type RemoveItemInput = {
id: string
}
-const useRemoveItem = useAction
+export type UseRemoveItem<
+ H extends MutationHook = Prop<
- Prop ,
- 'useCustomer'
->
+export type UseCustomer<
+ H extends SWRHook = UseHookInput<
- UseCustomerHandler
->
+export const fetcher: HookFetcherFn = UseHookResponse<
- UseCustomerHandler
->
+const fn = (provider: Provider) => provider.customer?.useCustomer!
-export type UseCustomer = Partial<
- UseCustomerInput
-> extends UseCustomerInput
- ? (input?: UseCustomerInput ) => CustomerResponse
- : (input: UseCustomerInput ) => CustomerResponse
-
-export const fetcher = defaultFetcher as HookFetcherFn (
- input: UseCustomerInput = {}
-) {
- const { providerRef, fetcherRef } = useCommerce ()
-
- const provider = providerRef.current
- const opts = provider.customer?.useCustomer
-
- const fetcherFn = opts?.fetcher ?? fetcher
- const useHook = opts?.useHook ?? ((ctx) => ctx.useData())
-
- return useHook({
- input,
- useData(ctx) {
- const response = useData(
- { ...opts!, fetcher: fetcherFn },
- ctx?.input ?? [],
- provider.fetcher ?? fetcherRef.current,
- ctx?.swrOptions ?? input.swrOptions
- )
- return response
- },
- })
+const useCustomer: UseCustomer = (input) => {
+ const hook = useHook(fn)
+ return useSWRHook({ fetcher, ...hook })(input)
}
+
+export default useCustomer
diff --git a/framework/commerce/index.tsx b/framework/commerce/index.tsx
index 243fba2db..07bf74a22 100644
--- a/framework/commerce/index.tsx
+++ b/framework/commerce/index.tsx
@@ -6,7 +6,7 @@ import {
useMemo,
useRef,
} from 'react'
-import { Fetcher, HookHandler, MutationHandler } from './utils/types'
+import { Fetcher, SWRHook, MutationHook } from './utils/types'
import type { FetchCartInput } from './cart/use-cart'
import type { Cart, Wishlist, Customer, SearchProductsData } from './types'
@@ -15,17 +15,26 @@ const Commerce = createContext = Prop<
- Prop ,
- 'useSearch'
->
+export type UseSearch<
+ H extends SWRHook = UseHookInput<
- UseSearchHandler
->
+export const fetcher: HookFetcherFn = UseHookResponse<
- UseSearchHandler
->
+const fn = (provider: Provider) => provider.products?.useSearch!
-export type UseSearch = Partial<
- UseSeachInput
-> extends UseSeachInput
- ? (input?: UseSeachInput ) => SearchResponse
- : (input: UseSeachInput ) => SearchResponse
-
-export const fetcher = defaultFetcher as HookFetcherFn (
- input: UseSeachInput = {}
-) {
- const { providerRef, fetcherRef } = useCommerce ()
-
- const provider = providerRef.current
- const opts = provider.products?.useSearch
-
- const fetcherFn = opts?.fetcher ?? fetcher
- const useHook = opts?.useHook ?? ((ctx) => ctx.useData())
-
- return useHook({
- input,
- useData(ctx) {
- const response = useData(
- { ...opts!, fetcher: fetcherFn },
- ctx?.input ?? [],
- provider.fetcher ?? fetcherRef.current,
- ctx?.swrOptions ?? input.swrOptions
- )
- return response
- },
- })
+const useSearch: UseSearch = (input) => {
+ const hook = useHook(fn)
+ return useSWRHook({ fetcher, ...hook })(input)
}
+
+export default useSearch
diff --git a/framework/commerce/types.ts b/framework/commerce/types.ts
index 0ae766095..86361fd9f 100644
--- a/framework/commerce/types.ts
+++ b/framework/commerce/types.ts
@@ -1,10 +1,6 @@
-import type { Wishlist as BCWishlist } from '@framework/api/wishlist'
-import type { Customer as BCCustomer } from '@framework/api/customers'
-import type { SearchProductsData as BCSearchProductsData } from '@framework/api/catalog/products'
-
-export type CommerceProviderConfig = {
- features: Record ()
+ const provider = providerRef.current
+ return fn(provider)
+}
+
+export function useSWRHook = Prop<
- Prop ,
- 'useWishlist'
->
+export type UseWishlist<
+ H extends SWRHook = UseHookInput<
- UseWishlistHandler
->
+export const fetcher: HookFetcherFn = UseHookResponse<
- UseWishlistHandler
->
+const fn = (provider: Provider) => provider.wishlist?.useWishlist!
-export type UseWishlist = Partial<
- UseWishlistInput
-> extends UseWishlistInput
- ? (input?: UseWishlistInput ) => WishlistResponse
- : (input: UseWishlistInput ) => WishlistResponse
-
-export const fetcher = defaultFetcher as HookFetcherFn (
- input: UseWishlistInput = {}
-) {
- const { providerRef, fetcherRef } = useCommerce ()
-
- const provider = providerRef.current
- const opts = provider.wishlist?.useWishlist
-
- const fetcherFn = opts?.fetcher ?? fetcher
- const useHook = opts?.useHook ?? ((ctx) => ctx.useData())
-
- return useHook({
- input,
- useData(ctx) {
- const response = useData(
- { ...opts!, fetcher: fetcherFn },
- ctx?.input ?? [],
- provider.fetcher ?? fetcherRef.current,
- ctx?.swrOptions ?? input.swrOptions
- )
- return response
- },
- })
+const useWishlist: UseWishlist = (input) => {
+ const hook = useHook(fn)
+ return useSWRHook({ fetcher, ...hook })(input)
}
+
+export default useWishlist
diff --git a/framework/shopify/.env.template b/framework/shopify/.env.template
new file mode 100644
index 000000000..74f446835
--- /dev/null
+++ b/framework/shopify/.env.template
@@ -0,0 +1,4 @@
+COMMERCE_PROVIDER=shopify
+
+NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=
+NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=
diff --git a/framework/shopify/README.md b/framework/shopify/README.md
new file mode 100644
index 000000000..d67111a41
--- /dev/null
+++ b/framework/shopify/README.md
@@ -0,0 +1,123 @@
+## Shopify Provider
+
+**Demo:** https://shopify.demo.vercel.store/
+
+Before getting starter, a [Shopify](https://www.shopify.com/) account and store is required before using the provider.
+
+Next, copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git):
+
+```bash
+cp framework/shopify/.env.template .env.local
+```
+
+Then, set the environment variables in `.env.local` to match the ones from your store.
+
+## Contribute
+
+Our commitment to Open Source can be found [here](https://vercel.com/oss).
+
+If you find an issue with the provider or want a new feature, feel free to open a PR or [create a new issue](https://github.com/vercel/commerce/issues).
+
+## Modifications
+
+These modifications are temporarily until contributions are made to remove them.
+
+### Adding item to Cart
+
+```js
+// components/product/ProductView/ProductView.tsx
+const ProductView: FC