Merge branch 'agnostic' of https://github.com/cond0r/commerce into shopify

This commit is contained in:
cond0r 2021-03-04 13:39:01 +02:00
commit 9f7a67e0d4
48 changed files with 707 additions and 378 deletions

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false
}

107
README.md
View File

@ -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 <b>under development</b>.
- Shopify Demo: https://shopify.demo.vercel.store/
- BigCommerce Demo: https://bigcommerce.demo.vercel.store/
## Features
@ -21,34 +22,22 @@ This project is currently <b>under development</b>.
- 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.
- **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.
There is a `framework` folder in the root folder that will contain multiple ecommerce providers.
## Provider Structure
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.
## Framework
Framework is where the data comes from. It contains mostly hooks and functions.
## Structure
Main folder and its exposed functions
Next.js Commerce provides a set of utilities and functions to create new providers. This is how a provider structure looks like.
- `product`
- usePrice
@ -68,39 +57,73 @@ Main folder and its exposed functions
- getCustomerId
- getCustomerWistlist
- `cart`
- useCart
- useAddItem
- useRemoveItem
- useUpdateItem
- `env.template`
- `provider.ts`
- `commerce.config.json`
- `next.config.js`
- `README.md`
- `config.json`
- README.md
## Configuration
#### Example of correct usage of Commerce Framework
### How to change providers
```js
import { useUI } from '@components/ui'
import { useCustomer } from '@framework/customer'
import { useWishlist, useAddItem, useRemoveItem } from '@framework/wishlist'
```
First, update the provider selected in `commerce.config.json`:
## Config
### Features
In order to make the UI entirely functional, we need to specify which features certain providers do not **provide**.
**Disabling wishlist:**
```
```json
{
"provider": "bigcommerce",
"features": {
"wishlist": false
"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)
People actively working on this project: @okbel & @lfades.
## Contribute
Our commitment to Open Source can be found [here](https://vercel.com/oss).

View File

@ -7,7 +7,7 @@
}
.productDisplay {
@apply relative flex px-0 pb-0 relative box-border col-span-1 bg-violet;
@apply relative flex px-0 pb-0 box-border col-span-1 bg-violet;
min-height: 600px;
@screen md {

View File

@ -1,4 +1,5 @@
.root {
composes: root from 'components/ui/Button/Button.module.css';
@apply h-12 w-12 bg-primary text-primary rounded-full mr-3 inline-flex
items-center justify-center cursor-pointer transition duration-150 ease-in-out
p-0 shadow-none border-gray-200 border box-border;

View File

@ -8,14 +8,18 @@ export type SelectedOptions = {
export function getVariant(product: Product, opts: SelectedOptions) {
const variant = product.variants.find((variant) => {
return Object.entries(opts).every(([key, value]) =>
variant.options.find((option) => {
if (
option.__typename === 'MultipleChoiceOption' &&
option.displayName.toLowerCase() === key.toLowerCase()
) {
return option.values.find((v) => v.label.toLowerCase() === value)
}
})
value
? variant.options.find((option) => {
if (
option.__typename === 'MultipleChoiceOption' &&
option.displayName.toLowerCase() === key.toLowerCase()
) {
return option.values.find((v) => v.label.toLowerCase() === value)
}
})
: !variant.options.find(
(v) => v.displayName.toLowerCase() === key.toLowerCase()
)
)
})
return variant

View File

@ -1,12 +1,22 @@
{
"title": "ACME Storefront | Powered by Next.js Commerce",
"titleTemplate": "%s - ACME Storefront",
"description": "Next.js Commerce -> https://www.nextjs.org/commerce",
"description": "Next.js Commerce - https://www.nextjs.org/commerce",
"openGraph": {
"title": "ACME Storefront | Powered by Next.js Commerce",
"description": "Next.js Commerce - https://www.nextjs.org/commerce",
"type": "website",
"locale": "en_IE",
"url": "https://nextjs.org/commerce",
"site_name": "Next.js Commerce"
"site_name": "Next.js Commerce",
"images": [
{
"url": "/card.png",
"width": 800,
"height": 600,
"alt": "Next.js Commerce"
}
]
},
"twitter": {
"handle": "@nextjs",

View File

@ -7,6 +7,7 @@ const merge = require('deepmerge')
const PROVIDERS = ['bigcommerce', 'shopify']
function getProviderName() {
// TODO: OSOT.
return process.env.BIGCOMMERCE_STOREFRONT_API_URL ? 'bigcommerce' : null
}

View File

@ -1,2 +1,2 @@
SHOPIFY_STORE_DOMAIN=
SHOPIFY_STOREFRONT_ACCESS_TOKEN=
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=

View File

@ -1,21 +0,0 @@
import Client from 'shopify-buy'
import { ShopifyConfig } from '../index'
type Options = {
config: ShopifyConfig
}
const getAllCollections = async (options: Options) => {
const { config } = options
const client = Client.buildClient({
storefrontAccessToken: config.apiToken,
domain: config.commerceUrl,
})
const res = await client.collection.fetchAllWithProducts()
return JSON.parse(JSON.stringify(res))
}
export default getAllCollections

View File

@ -1,25 +0,0 @@
import { Page } from '../../schema'
import { ShopifyConfig, getConfig } from '..'
export type GetPageResult<T extends { page?: any } = { page?: Page }> = T
export type PageVariables = {
id: string
}
async function getPage({
url,
variables,
config,
preview,
}: {
url?: string
variables: PageVariables
config?: ShopifyConfig
preview?: boolean
}): Promise<GetPageResult> {
config = getConfig(config)
return {}
}
export default getPage

View File

@ -10,7 +10,7 @@ import {
MutationCheckoutCreateArgs,
} from '../schema'
import useLogin, { UseLogin } from '@commerce/auth/use-login'
import { setCustomerToken } from '../utils'
import { setCustomerToken, throwUserErrors } from '../utils'
export default useLogin as UseLogin<typeof handler>
@ -45,13 +45,8 @@ export const handler: MutationHook<null, {}, CustomerAccessTokenCreateInput> = {
},
})
const errors = customerAccessTokenCreate?.customerUserErrors
throwUserErrors(customerAccessTokenCreate?.customerUserErrors)
if (errors && errors.length) {
throw new ValidationError({
message: getErrorMessage(errors[0]),
})
}
const customerAccessToken = customerAccessTokenCreate?.customerAccessToken
const accessToken = customerAccessToken?.accessToken

View File

@ -1,15 +1,16 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import { CommerceError, ValidationError } from '@commerce/utils/errors'
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
import useCustomer from '../customer/use-customer'
import { CustomerCreateInput } from '../schema'
import {
customerCreateMutation,
customerAccessTokenCreateMutation,
} from '../utils/mutations'
import handleLogin from '../utils/handle-login'
CustomerCreateInput,
Mutation,
MutationCustomerCreateArgs,
} from '../schema'
import { customerCreateMutation } from '../utils/mutations'
import { handleAutomaticLogin, throwUserErrors } from '../utils'
export default useSignup as UseSignup<typeof handler>
@ -33,7 +34,11 @@ export const handler: MutationHook<
'A first name, last name, email and password are required to signup',
})
}
const data = await fetch({
const { customerCreate } = await fetch<
Mutation,
MutationCustomerCreateArgs
>({
...options,
variables: {
input: {
@ -45,19 +50,10 @@ export const handler: MutationHook<
},
})
try {
const loginData = await fetch({
query: customerAccessTokenCreateMutation,
variables: {
input: {
email,
password,
},
},
})
handleLogin(loginData)
} catch (error) {}
return data
throwUserErrors(customerCreate?.customerUserErrors)
await handleAutomaticLogin(fetch, { email, password })
return null
},
useHook: ({ fetch }) => () => {
const { revalidate } = useCustomer()

View File

@ -1,3 +1,4 @@
export { default as useCart } from './use-cart'
export { default as useAddItem } from './use-add-item'
export { default as useUpdateItem } from './use-update-item'
export { default as useRemoveItem } from './use-remove-item'

View File

@ -1,12 +1,15 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
import useCart from './use-cart'
import {
checkoutLineItemAddMutation,
getCheckoutId,
checkoutToCart,
} from '../utils'
import { Cart, CartItemBody } from '../types'
import { checkoutLineItemAddMutation, getCheckoutId } from '../utils'
import { checkoutToCart } from './utils'
import { Mutation, MutationCheckoutLineItemsAddArgs } from '../schema'
import { useCallback } from 'react'
export default useAddItem as UseAddItem<typeof handler>
@ -40,8 +43,7 @@ export const handler: MutationHook<Cart, {}, CartItemBody> = {
},
})
// TODO: Fix this Cart type here
return checkoutToCart(checkoutLineItemsAdd) as any
return checkoutToCart(checkoutLineItemsAdd)
},
useHook: ({ fetch }) => () => {
const { mutate } = useCart()

View File

@ -6,7 +6,7 @@ import useCommerceCart, {
import { Cart } from '../types'
import { SWRHook } from '@commerce/utils/types'
import { checkoutCreate, checkoutToCart } from './utils'
import { checkoutCreate, checkoutToCart } from '../utils'
import getCheckoutQuery from '../utils/queries/get-checkout-query'
export default useCommerceCart as UseCart<typeof handler>
@ -22,6 +22,7 @@ export const handler: SWRHook<
},
async fetcher({ input: { cartId: checkoutId }, options, fetch }) {
let checkout
if (checkoutId) {
const data = await fetch({
...options,
@ -36,8 +37,7 @@ export const handler: SWRHook<
checkout = await checkoutCreate(fetch)
}
// TODO: Fix this type
return checkoutToCart({ checkout } as any)
return checkoutToCart({ checkout })
},
useHook: ({ useData }) => (input) => {
const response = useData({

View File

@ -1,23 +1,22 @@
import { useCallback } from 'react'
import type {
MutationHookContext,
HookFetcherContext,
} from '@commerce/utils/types'
import { RemoveCartItemBody } from '@commerce/types'
import { ValidationError } from '@commerce/utils/errors'
import useRemoveItem, {
RemoveItemInput as RemoveItemInputBase,
UseRemoveItem,
} from '@commerce/cart/use-remove-item'
import useCart from './use-cart'
import { checkoutLineItemRemoveMutation, getCheckoutId } from '../utils'
import { checkoutToCart } from './utils'
import {
checkoutLineItemRemoveMutation,
getCheckoutId,
checkoutToCart,
} from '../utils'
import { Cart, LineItem } from '../types'
import { Mutation, MutationCheckoutLineItemsRemoveArgs } from '../schema'
import { RemoveCartItemBody } from '@commerce/types'
export type RemoveItemFn<T = any> = T extends LineItem
? (input?: RemoveItemInput<T>) => Promise<Cart | null>

View File

@ -13,7 +13,7 @@ import useUpdateItem, {
import useCart from './use-cart'
import { handler as removeItemHandler } from './use-remove-item'
import type { Cart, LineItem, UpdateCartItemBody } from '../types'
import { checkoutToCart } from './utils'
import { checkoutToCart } from '../utils'
import { getCheckoutId, checkoutLineItemUpdateMutation } from '../utils'
import { Mutation, MutationCheckoutLineItemsUpdateArgs } from '../schema'

View File

@ -1,42 +0,0 @@
import { Cart } from '../../types'
import { CommerceError, ValidationError } from '@commerce/utils/errors'
import {
CheckoutLineItemsAddPayload,
CheckoutLineItemsRemovePayload,
CheckoutLineItemsUpdatePayload,
Maybe,
} from '../../schema'
import { normalizeCart } from '../../utils'
export type CheckoutPayload =
| CheckoutLineItemsAddPayload
| CheckoutLineItemsUpdatePayload
| CheckoutLineItemsRemovePayload
const checkoutToCart = (checkoutPayload?: Maybe<CheckoutPayload>): Cart => {
if (!checkoutPayload) {
throw new CommerceError({
message: 'Invalid response from Shopify',
})
}
const checkout = checkoutPayload?.checkout
const userErrors = checkoutPayload?.userErrors
if (userErrors && userErrors.length) {
throw new ValidationError({
message: userErrors[0].message,
})
}
if (!checkout) {
throw new CommerceError({
message: 'Invalid response from Shopify',
})
}
return normalizeCart(checkout)
}
export default checkoutToCart

View File

@ -1,31 +0,0 @@
import { HookFetcherFn } from '@commerce/utils/types'
import { Cart } from '@commerce/types'
import { checkoutCreate, checkoutToCart } from '.'
import { FetchCartInput } from '@commerce/cart/use-cart'
const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
options,
input: { cartId: checkoutId },
fetch,
}) => {
let checkout
if (checkoutId) {
const data = await fetch({
...options,
variables: {
checkoutId,
},
})
checkout = data.node
}
if (checkout?.completedAt || !checkoutId) {
checkout = await checkoutCreate(fetch)
}
// TODO: Fix this type
return checkoutToCart({ checkout } as any)
}
export default fetcher

View File

@ -1,2 +0,0 @@
export { default as checkoutToCart } from './checkout-to-cart'
export { default as checkoutCreate } from './checkout-create'

View File

@ -48,7 +48,8 @@ export const handler: SWRHook<
edges = data.node?.products?.edges ?? []
if (brandId) {
edges = edges.filter(
({ node: { vendor } }: ProductEdge) => vendor === brandId
({ node: { vendor } }: ProductEdge) =>
vendor.replace(/\s+/g, '-').toLowerCase() === brandId
)
}
} else {

View File

@ -23,9 +23,6 @@ export const shopifyProvider = {
customer: { useCustomer },
products: { useSearch },
auth: { useLogin, useLogout, useSignup },
features: {
wishlist: false,
},
}
export type ShopifyProvider = typeof shopifyProvider

View File

@ -1,13 +1,17 @@
import Cookies from 'js-cookie'
import {
SHOPIFY_CHECKOUT_ID_COOKIE,
SHOPIFY_CHECKOUT_URL_COOKIE,
SHOPIFY_COOKIE_EXPIRE,
} from '../../const'
} from '../const'
import checkoutCreateMutation from '../../utils/mutations/checkout-create'
import Cookies from 'js-cookie'
import checkoutCreateMutation from './mutations/checkout-create'
import { CheckoutCreatePayload } from '../schema'
export const checkoutCreate = async (fetch: any) => {
export const checkoutCreate = async (
fetch: any
): Promise<CheckoutCreatePayload> => {
const data = await fetch({
query: checkoutCreateMutation,
})
@ -20,7 +24,7 @@ export const checkoutCreate = async (fetch: any) => {
expires: SHOPIFY_COOKIE_EXPIRE,
}
Cookies.set(SHOPIFY_CHECKOUT_ID_COOKIE, checkoutId, options)
Cookies.set(SHOPIFY_CHECKOUT_URL_COOKIE, checkout?.webUrl, options)
Cookies.set(SHOPIFY_CHECKOUT_URL_COOKIE, checkout.webUrl, options)
}
return checkout

View File

@ -0,0 +1,48 @@
import { Cart } from '../types'
import { CommerceError } from '@commerce/utils/errors'
import {
CheckoutLineItemsAddPayload,
CheckoutLineItemsRemovePayload,
CheckoutLineItemsUpdatePayload,
CheckoutCreatePayload,
CheckoutUserError,
Checkout,
Maybe,
} from '../schema'
import { normalizeCart } from './normalize'
import throwUserErrors from './throw-user-errors'
export type CheckoutQuery = {
checkout: Checkout
checkoutUserErrors?: Array<CheckoutUserError>
}
export type CheckoutPayload =
| CheckoutLineItemsAddPayload
| CheckoutLineItemsUpdatePayload
| CheckoutLineItemsRemovePayload
| CheckoutCreatePayload
| CheckoutQuery
const checkoutToCart = (checkoutPayload?: Maybe<CheckoutPayload>): Cart => {
if (!checkoutPayload) {
throw new CommerceError({
message: 'Missing checkout payload from response',
})
}
const checkout = checkoutPayload?.checkout
throwUserErrors(checkoutPayload?.checkoutUserErrors)
if (!checkout) {
throw new CommerceError({
message: 'Missing checkout object from response',
})
}
return normalizeCart(checkout)
}
export default checkoutToCart

View File

@ -1,4 +1,4 @@
const getSortVariables = (sort?: string, isCategory = false) => {
const getSortVariables = (sort?: string, isCategory: boolean = false) => {
let output = {}
switch (sort) {
case 'price-asc':

View File

@ -2,13 +2,14 @@ import { ShopifyConfig } from '../api'
import fetchAllProducts from '../api/utils/fetch-all-products'
import getAllProductVendors from './queries/get-all-product-vendors-query'
export type BrandNode = {
export type Brand = {
entityId: string
name: string
path: string
}
export type BrandEdge = {
node: BrandNode
node: Brand
}
export type Brands = BrandEdge[]
@ -24,13 +25,16 @@ const getVendors = async (config: ShopifyConfig): Promise<BrandEdge[]> => {
let vendorsStrings = vendors.map(({ node: { vendor } }) => vendor)
return [...new Set(vendorsStrings)].map((v) => ({
node: {
entityId: v,
name: v,
path: `brands/${v}`,
},
}))
return [...new Set(vendorsStrings)].map((v) => {
const id = v.replace(/\s+/g, '-').toLowerCase()
return {
node: {
entityId: id,
name: v,
path: `brands/${id}`,
},
}
})
}
export default getVendors

View File

@ -0,0 +1,30 @@
import { FetcherOptions } from '@commerce/utils/types'
import throwUserErrors from './throw-user-errors'
import {
MutationCustomerActivateArgs,
MutationCustomerActivateByUrlArgs,
} from '../schema'
import { Mutation } from '../schema'
import { customerActivateByUrlMutation } from './mutations'
const handleAccountActivation = async (
fetch: <T = any, B = Body>(options: FetcherOptions<B>) => Promise<T>,
input: MutationCustomerActivateByUrlArgs
) => {
try {
const { customerActivateByUrl } = await fetch<
Mutation,
MutationCustomerActivateArgs
>({
query: customerActivateByUrlMutation,
variables: {
input,
},
})
throwUserErrors(customerActivateByUrl?.customerUserErrors)
} catch (error) {}
}
export default handleAccountActivation

View File

@ -1,30 +1,12 @@
import { ValidationError } from '@commerce/utils/errors'
import { FetcherOptions } from '@commerce/utils/types'
import { CustomerAccessTokenCreateInput } from '../schema'
import { setCustomerToken } from './customer-token'
const getErrorMessage = ({
code,
message,
}: {
code: string
message: string
}) => {
switch (code) {
case 'UNIDENTIFIED_CUSTOMER':
message = 'Cannot find an account that matches the provided credentials'
break
}
return message
}
import { customerAccessTokenCreateMutation } from './mutations'
import throwUserErrors from './throw-user-errors'
const handleLogin = (data: any) => {
const response = data.customerAccessTokenCreate
const errors = response?.customerUserErrors
if (errors && errors.length) {
throw new ValidationError({
message: getErrorMessage(errors[0]),
})
}
throwUserErrors(response?.customerUserErrors)
const customerAccessToken = response?.customerAccessToken
const accessToken = customerAccessToken?.accessToken
@ -36,4 +18,19 @@ const handleLogin = (data: any) => {
return customerAccessToken
}
export const handleAutomaticLogin = async (
fetch: <T = any, B = Body>(options: FetcherOptions<B>) => Promise<T>,
input: CustomerAccessTokenCreateInput
) => {
try {
const loginData = await fetch({
query: customerAccessTokenCreateMutation,
variables: {
input,
},
})
handleLogin(loginData)
} catch (error) {}
}
export default handleLogin

View File

@ -4,6 +4,11 @@ export { default as getSortVariables } from './get-sort-variables'
export { default as getVendors } from './get-vendors'
export { default as getCategories } from './get-categories'
export { default as getCheckoutId } from './get-checkout-id'
export { default as checkoutCreate } from './checkout-create'
export { default as checkoutToCart } from './checkout-to-cart'
export { default as handleLogin, handleAutomaticLogin } from './handle-login'
export { default as handleAccountActivation } from './handle-account-activation'
export { default as throwUserErrors } from './throw-user-errors'
export * from './queries'
export * from './mutations'
export * from './normalize'

View File

@ -3,9 +3,10 @@ import { checkoutDetailsFragment } from '../queries/get-checkout-query'
const checkoutCreateMutation = /* GraphQL */ `
mutation {
checkoutCreate(input: {}) {
userErrors {
message
checkoutUserErrors {
code
field
message
}
checkout {
${checkoutDetailsFragment}

View File

@ -3,9 +3,10 @@ import { checkoutDetailsFragment } from '../queries/get-checkout-query'
const checkoutLineItemAddMutation = /* GraphQL */ `
mutation($checkoutId: ID!, $lineItems: [CheckoutLineItemInput!]!) {
checkoutLineItemsAdd(checkoutId: $checkoutId, lineItems: $lineItems) {
userErrors {
message
checkoutUserErrors {
code
field
message
}
checkout {
${checkoutDetailsFragment}

View File

@ -6,9 +6,10 @@ const checkoutLineItemRemoveMutation = /* GraphQL */ `
checkoutId: $checkoutId
lineItemIds: $lineItemIds
) {
userErrors {
message
checkoutUserErrors {
code
field
message
}
checkout {
${checkoutDetailsFragment}

View File

@ -3,9 +3,10 @@ import { checkoutDetailsFragment } from '../queries/get-checkout-query'
const checkoutLineItemUpdateMutation = /* GraphQL */ `
mutation($checkoutId: ID!, $lineItems: [CheckoutLineItemUpdateInput!]!) {
checkoutLineItemsUpdate(checkoutId: $checkoutId, lineItems: $lineItems) {
userErrors {
message
checkoutUserErrors {
code
field
message
}
checkout {
${checkoutDetailsFragment}

View File

@ -3,7 +3,7 @@ const customerAccessTokenDeleteMutation = /* GraphQL */ `
customerAccessTokenDelete(customerAccessToken: $customerAccessToken) {
deletedAccessToken
deletedCustomerAccessTokenId
userErrors {
customerUserErrors {
field
message
}

View File

@ -0,0 +1,19 @@
const customerActivateByUrlMutation = /* GraphQL */ `
mutation customerActivateByUrl($activationUrl: URL!, $password: String!) {
customerActivateByUrl(activationUrl: $activationUrl, password: $password) {
customer {
id
}
customerAccessToken {
accessToken
expiresAt
}
customerUserErrors {
code
field
message
}
}
}
`
export default customerActivateByUrlMutation

View File

@ -0,0 +1,19 @@
const customerActivateMutation = /* GraphQL */ `
mutation customerActivate($id: ID!, $input: CustomerActivateInput!) {
customerActivate(id: $id, input: $input) {
customer {
id
}
customerAccessToken {
accessToken
expiresAt
}
customerUserErrors {
code
field
message
}
}
}
`
export default customerActivateMutation

View File

@ -5,3 +5,5 @@ export { default as checkoutLineItemUpdateMutation } from './checkout-line-item-
export { default as checkoutLineItemRemoveMutation } from './checkout-line-item-remove'
export { default as customerAccessTokenCreateMutation } from './customer-access-token-create'
export { default as customerAccessTokenDeleteMutation } from './customer-access-token-delete'
export { default as customerActivateMutation } from './customer-activate'
export { default as customerActivateByUrlMutation } from './customer-activate-by-url'

View File

@ -33,7 +33,7 @@ const normalizeProductOption = ({
let output: any = {
label: value,
}
if (displayName === 'Color') {
if (displayName.match(/colou?r/gi)) {
output = {
...output,
hexColors: [value],
@ -54,21 +54,24 @@ const normalizeProductVariants = ({ edges }: ProductVariantConnection) => {
return edges?.map(
({
node: { id, selectedOptions, sku, title, priceV2, compareAtPriceV2 },
}) => ({
id,
name: title,
sku: sku ?? id,
price: +priceV2.amount,
listPrice: +compareAtPriceV2?.amount,
requiresShipping: true,
options: selectedOptions.map(({ name, value }: SelectedOption) =>
normalizeProductOption({
id,
name,
values: [value],
})
),
})
}) => {
return {
id,
name: title,
sku: sku ?? id,
price: +priceV2.amount,
listPrice: +compareAtPriceV2?.amount,
requiresShipping: true,
options: selectedOptions.map(({ name, value }: SelectedOption) => {
const options = normalizeProductOption({
id,
name,
values: [value],
})
return options
}),
}
}
)
}
@ -96,7 +99,12 @@ export function normalizeProduct(productNode: ShopifyProduct): Product {
price: money(priceRange?.minVariantPrice),
images: normalizeProductImages(images),
variants: variants ? normalizeProductVariants(variants) : [],
options: options ? options.map((o) => normalizeProductOption(o)) : [],
options:
options
?.filter(({ name, values }) => {
return name !== 'Title' && values !== ['Default Title']
})
.map((o) => normalizeProductOption(o)) ?? [],
...rest,
}
@ -122,7 +130,7 @@ export function normalizeCart(checkout: Checkout): Cart {
}
function normalizeLineItem({
node: { id, title, variant, quantity },
node: { id, title, variant, quantity, ...rest },
}: CheckoutLineItemEdge): LineItem {
return {
id,
@ -135,7 +143,7 @@ function normalizeLineItem({
sku: variant?.sku ?? '',
name: variant?.title!,
image: {
url: variant?.image?.originalSrc,
url: variant?.image?.originalSrc ?? '/product-img-placeholder.svg',
},
requiresShipping: variant?.requiresShipping ?? false,
price: variant?.priceV2?.amount,
@ -143,10 +151,13 @@ function normalizeLineItem({
},
path: '',
discounts: [],
options: [
{
value: variant?.title,
},
],
options:
variant?.title !== 'Default Title'
? [
{
value: variant?.title,
},
]
: [],
}
}

View File

@ -0,0 +1,38 @@
import { ValidationError } from '@commerce/utils/errors'
import {
CheckoutErrorCode,
CheckoutUserError,
CustomerErrorCode,
CustomerUserError,
} from '../schema'
export type UserErrors = Array<CheckoutUserError | CustomerUserError>
export type UserErrorCode =
| CustomerErrorCode
| CheckoutErrorCode
| null
| undefined
const getCustomMessage = (code: UserErrorCode, message: string) => {
switch (code) {
case 'UNIDENTIFIED_CUSTOMER':
message = 'Cannot find an account that matches the provided credentials'
break
}
return message
}
export const throwUserErrors = (errors?: UserErrors) => {
if (errors && errors.length) {
throw new ValidationError({
errors: errors.map(({ code, message }) => ({
code: code ?? 'validation_error',
message: getCustomMessage(code, message),
})),
})
}
}
export default throwUserErrors

View File

@ -1,13 +1,34 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useAddItem, { UseAddItem } from '@commerce/wishlist/use-add-item'
export function emptyHook() {
const useEmptyHook = async (options = {}) => {
return useCallback(async function () {
return Promise.resolve()
}, [])
}
import useCustomer from '../customer/use-customer'
import useWishlist from './use-wishlist'
return useEmptyHook
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<any, {}, any, any> = {
fetchOptions: {
query: '',
},
useHook: ({ fetch }) => () => {
const { data: customer } = useCustomer()
const { revalidate } = useWishlist()
return useCallback(
async function addItem(item) {
if (!customer) {
// A signed customer is required in order to have a wishlist
throw new CommerceError({
message: 'Signed customer not found',
})
}
await revalidate()
return null
},
[fetch, revalidate, customer]
)
},
}
export default emptyHook

View File

@ -1,17 +1,36 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useRemoveItem, {
UseRemoveItem,
} from '@commerce/wishlist/use-remove-item'
type Options = {
includeProducts?: boolean
import useCustomer from '../customer/use-customer'
import useWishlist from './use-wishlist'
export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler: MutationHook<any, {}, any, any> = {
fetchOptions: {
query: '',
},
useHook: ({ fetch }) => () => {
const { data: customer } = useCustomer()
const { revalidate } = useWishlist()
return useCallback(
async function addItem(item) {
if (!customer) {
// A signed customer is required in order to have a wishlist
throw new CommerceError({
message: 'Signed customer not found',
})
}
await revalidate()
return null
},
[fetch, revalidate, customer]
)
},
}
export function emptyHook(options?: Options) {
const useEmptyHook = async ({ id }: { id: string | number }) => {
return useCallback(async function () {
return Promise.resolve()
}, [])
}
return useEmptyHook
}
export default emptyHook

View File

@ -1,46 +1,49 @@
// TODO: replace this hook and other wishlist hooks with a handler, or remove them if
// Shopify doesn't have a wishlist
import { useMemo } from 'react'
import { SWRHook } from '@commerce/utils/types'
import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist'
import useCustomer from '../customer/use-customer'
import { HookFetcher } from '@commerce/utils/types'
import { Product } from '../schema'
export type UseWishlistInput = { includeProducts?: boolean }
const defaultOpts = {}
export default useWishlist as UseWishlist<typeof handler>
export type Wishlist = {
items: [
{
product_id: number
variant_id: number
id: number
product: Product
}
]
export const handler: SWRHook<
any | null,
UseWishlistInput,
{ customerId?: number } & UseWishlistInput,
{ isEmpty?: boolean }
> = {
fetchOptions: {
url: '/api/bigcommerce/wishlist',
method: 'GET',
},
fetcher() {
return { items: [] }
},
useHook: ({ useData }) => (input) => {
const { data: customer } = useCustomer()
const response = useData({
input: [
['customerId', customer?.entityId],
['includeProducts', input?.includeProducts],
],
swrOptions: {
revalidateOnFocus: false,
...input?.swrOptions,
},
})
return useMemo(
() =>
Object.create(response, {
isEmpty: {
get() {
return (response.data?.items?.length || 0) <= 0
},
enumerable: true,
},
}),
[response]
)
},
}
export interface UseWishlistOptions {
includeProducts?: boolean
}
export interface UseWishlistInput extends UseWishlistOptions {
customerId?: number
}
export const fetcher: HookFetcher<Wishlist | null, UseWishlistInput> = () => {
return null
}
export function extendHook(
customFetcher: typeof fetcher,
// swrOptions?: SwrOptions<Wishlist | null, UseWishlistInput>
swrOptions?: any
) {
const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => {
return { data: null }
}
useWishlist.extend = extendHook
return useWishlist
}
export default extendHook(fetcher)

View File

@ -42,9 +42,161 @@ function hexToRgb(hex: string = '') {
return [r, g, b]
}
export function isDark(color = '') {
const colorMap: Record<string, string> = {
aliceblue: '#F0F8FF',
antiquewhite: '#FAEBD7',
aqua: '#00FFFF',
aquamarine: '#7FFFD4',
azure: '#F0FFFF',
beige: '#F5F5DC',
bisque: '#FFE4C4',
black: '#000000',
blanchedalmond: '#FFEBCD',
blue: '#0000FF',
blueviolet: '#8A2BE2',
brown: '#A52A2A',
burlywood: '#DEB887',
cadetblue: '#5F9EA0',
chartreuse: '#7FFF00',
chocolate: '#D2691E',
coral: '#FF7F50',
cornflowerblue: '#6495ED',
cornsilk: '#FFF8DC',
crimson: '#DC143C',
cyan: '#00FFFF',
darkblue: '#00008B',
darkcyan: '#008B8B',
darkgoldenrod: '#B8860B',
darkgray: '#A9A9A9',
darkgreen: '#006400',
darkgrey: '#A9A9A9',
darkkhaki: '#BDB76B',
darkmagenta: '#8B008B',
darkolivegreen: '#556B2F',
darkorange: '#FF8C00',
darkorchid: '#9932CC',
darkred: '#8B0000',
darksalmon: '#E9967A',
darkseagreen: '#8FBC8F',
darkslateblue: '#483D8B',
darkslategray: '#2F4F4F',
darkslategrey: '#2F4F4F',
darkturquoise: '#00CED1',
darkviolet: '#9400D3',
deeppink: '#FF1493',
deepskyblue: '#00BFFF',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1E90FF',
firebrick: '#B22222',
floralwhite: '#FFFAF0',
forestgreen: '#228B22',
fuchsia: '#FF00FF',
gainsboro: '#DCDCDC',
ghostwhite: '#F8F8FF',
gold: '#FFD700',
goldenrod: '#DAA520',
gray: '#808080',
green: '#008000',
greenyellow: '#ADFF2F',
grey: '#808080',
honeydew: '#F0FFF0',
hotpink: '#FF69B4',
indianred: '#CD5C5C',
indigo: '#4B0082',
ivory: '#FFFFF0',
khaki: '#F0E68C',
lavender: '#E6E6FA',
lavenderblush: '#FFF0F5',
lawngreen: '#7CFC00',
lemonchiffon: '#FFFACD',
lightblue: '#ADD8E6',
lightcoral: '#F08080',
lightcyan: '#E0FFFF',
lightgoldenrodyellow: '#FAFAD2',
lightgray: '#D3D3D3',
lightgreen: '#90EE90',
lightgrey: '#D3D3D3',
lightpink: '#FFB6C1',
lightsalmon: '#FFA07A',
lightseagreen: '#20B2AA',
lightskyblue: '#87CEFA',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#B0C4DE',
lightyellow: '#FFFFE0',
lime: '#00FF00',
limegreen: '#32CD32',
linen: '#FAF0E6',
magenta: '#FF00FF',
maroon: '#800000',
mediumaquamarine: '#66CDAA',
mediumblue: '#0000CD',
mediumorchid: '#BA55D3',
mediumpurple: '#9370DB',
mediumseagreen: '#3CB371',
mediumslateblue: '#7B68EE',
mediumspringgreen: '#00FA9A',
mediumturquoise: '#48D1CC',
mediumvioletred: '#C71585',
midnightblue: '#191970',
mintcream: '#F5FFFA',
mistyrose: '#FFE4E1',
moccasin: '#FFE4B5',
navajowhite: '#FFDEAD',
navy: '#000080',
oldlace: '#FDF5E6',
olive: '#808000',
olivedrab: '#6B8E23',
orange: '#FFA500',
orangered: '#FF4500',
orchid: '#DA70D6',
palegoldenrod: '#EEE8AA',
palegreen: '#98FB98',
paleturquoise: '#AFEEEE',
palevioletred: '#DB7093',
papayawhip: '#FFEFD5',
peachpuff: '#FFDAB9',
peru: '#CD853F',
pink: '#FFC0CB',
plum: '#DDA0DD',
powderblue: '#B0E0E6',
purple: '#800080',
rebeccapurple: '#663399',
red: '#FF0000',
rosybrown: '#BC8F8F',
royalblue: '#4169E1',
saddlebrown: '#8B4513',
salmon: '#FA8072',
sandybrown: '#F4A460',
seagreen: '#2E8B57',
seashell: '#FFF5EE',
sienna: '#A0522D',
silver: '#C0C0C0',
skyblue: '#87CEEB',
slateblue: '#6A5ACD',
slategray: '#708090',
slategrey: '#708090',
snow: '#FFFAFA',
springgreen: '#00FF7F',
steelblue: '#4682B4',
tan: '#D2B48C',
teal: '#008080',
thistle: '#D8BFD8',
tomato: '#FF6347',
turquoise: '#40E0D0',
violet: '#EE82EE',
wheat: '#F5DEB3',
white: '#FFFFFF',
whitesmoke: '#F5F5F5',
yellow: '#FFFF00',
yellowgreen: '#9ACD32',
}
export function isDark(color: string = ''): boolean {
color = color.toLowerCase()
// Equation from http://24ways.org/2010/calculating-color-contrast
const rgb = hexToRgb(color)
let rgb = colorMap[color] ? hexToRgb(colorMap[color]) : hexToRgb(color)
const res = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000
return res < 128
}

View File

@ -14,15 +14,12 @@
"sideEffects": false,
"license": "MIT",
"engines": {
"node": "12.x"
},
"prettier": {
"semi": false,
"singleQuote": true
"node": "14.x"
},
"dependencies": {
"@reach/portal": "^0.11.2",
"@vercel/fetch": "^6.1.0",
"autoprefixer": "^10.2.4",
"body-scroll-lock": "^3.1.5",
"bowser": "^2.11.0",
"classnames": "^2.2.6",
@ -38,7 +35,7 @@
"next": "^10.0.7",
"next-seo": "^4.11.0",
"next-themes": "^0.0.4",
"postcss": "^8.2.4",
"postcss": "^8.2.6",
"postcss-nesting": "^7.0.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
@ -47,7 +44,7 @@
"shopify-buy": "^2.11.0",
"swr": "^0.4.0",
"tabbable": "^5.1.5",
"tailwindcss": "^2.0.2"
"tailwindcss": "^2.0.3"
},
"devDependencies": {
"@graphql-codegen/cli": "^1.20.0",
@ -73,7 +70,7 @@
"next-unused": "^0.0.3",
"postcss-flexbugs-fixes": "^4.2.1",
"postcss-preset-env": "^6.7.0",
"prettier": "^2.1.2",
"prettier": "^2.2.1",
"typescript": "^4.0.3"
},
"resolutions": {

View File

@ -0,0 +1,26 @@
import type { GetStaticPropsContext } from 'next'
import { getConfig } from '@framework/api'
import getAllPages from '@framework/common/get-all-pages'
import { Layout } from '@components/common'
import { Container, Text } from '@components/ui'
export async function getStaticProps({
preview,
locale,
}: GetStaticPropsContext) {
const config = getConfig({ locale })
const { pages } = await getAllPages({ config, preview })
return {
props: { pages },
}
}
export default function ActivateAccount() {
return (
<Container>
<Text variant="pageHeading">Activate Your Account</Text>
</Container>
)
}
ActivateAccount.Layout = Layout

View File

@ -75,10 +75,8 @@ export default function Search({
const { data } = useSearch({
search: typeof q === 'string' ? q : '',
// TODO: Shopify - Fix this type
categoryId: activeCategory?.entityId as any,
// TODO: Shopify - Fix this type
brandId: (activeBrand as any)?.entityId,
categoryId: activeCategory?.entityId,
brandId: activeBrand?.entityId,
sort: typeof sort === 'string' ? sort : '',
})

BIN
public/card.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -1609,6 +1609,18 @@ auto-bind@~4.0.0:
resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-4.0.0.tgz#e3589fc6c2da8f7ca43ba9f84fa52a744fc997fb"
integrity sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==
autoprefixer@^10.2.4:
version "10.2.4"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.2.4.tgz#c0e7cf24fcc6a1ae5d6250c623f0cb8beef2f7e1"
integrity sha512-DCCdUQiMD+P/as8m3XkeTUkUKuuRqLGcwD0nll7wevhqoJfMRpJlkFd1+MQh1pvupjiQuip42lc/VFvfUTMSKw==
dependencies:
browserslist "^4.16.1"
caniuse-lite "^1.0.30001181"
colorette "^1.2.1"
fraction.js "^4.0.13"
normalize-range "^0.1.2"
postcss-value-parser "^4.1.0"
autoprefixer@^9.6.1:
version "9.8.6"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f"
@ -1818,7 +1830,7 @@ browserslist@4.16.1:
escalade "^3.1.1"
node-releases "^1.1.69"
browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.6.4:
browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.16.1, browserslist@^4.6.4:
version "4.16.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717"
integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==
@ -3187,6 +3199,11 @@ form-data@^3.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"
fraction.js@^4.0.13:
version "4.0.13"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.13.tgz#3c1c315fa16b35c85fffa95725a36fa729c69dfe"
integrity sha512-E1fz2Xs9ltlUp+qbiyx9wmt2n9dRzPsS11Jtdb8D2o+cC7wr9xkkKsVKJuBX0ST+LVS+LhLO+SbLJNtfWcJvXA==
fs-capacitor@^6.1.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-6.2.0.tgz#fa79ac6576629163cb84561995602d8999afb7f5"
@ -5586,7 +5603,7 @@ postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.26, postcss@^7.0.
source-map "^0.6.1"
supports-color "^6.1.0"
postcss@^8.1.6, postcss@^8.2.1, postcss@^8.2.4:
postcss@^8.1.6, postcss@^8.2.1, postcss@^8.2.6:
version "8.2.6"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.6.tgz#5d69a974543b45f87e464bc4c3e392a97d6be9fe"
integrity sha512-xpB8qYxgPuly166AGlpRjUdEYtmOWx2iCwGmrv4vqZL9YPVviDVPZPRXxnXr6xPZOdxQ9lp3ZBFCRgWJ7LE3Sg==
@ -5645,7 +5662,7 @@ prepend-http@^2.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
prettier@^2.0.5, prettier@^2.1.2:
prettier@^2.0.5, prettier@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5"
integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==
@ -6617,7 +6634,7 @@ tabbable@^5.1.5:
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.1.5.tgz#efec48ede268d511c261e3b81facbb4782a35147"
integrity sha512-oVAPrWgLLqrbvQE8XqcU7CVBq6SQbaIbHkhOca3u7/jzuQvyZycrUKPCGr04qpEIUslmUlULbSeN+m3QrKEykA==
tailwindcss@^2.0.2:
tailwindcss@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-2.0.3.tgz#f8d07797d1f89dc4b171673c26237b58783c2c86"
integrity sha512-s8NEqdLBiVbbdL0a5XwTb8jKmIonOuI4RMENEcKLR61jw6SdKvBss7NWZzwCaD+ZIjlgmesv8tmrjXEp7C0eAQ==