From 49508a5e81165854c7b519a014c504e9f80245c3 Mon Sep 17 00:00:00 2001 From: goncy Date: Sun, 18 Apr 2021 11:52:51 -0300 Subject: [PATCH] CSV WIP --- .env.template | 2 + framework/commerce/config.js | 2 +- framework/csv/.env.template | 3 + framework/csv/README.md | 3 + framework/csv/api/cart/index.ts | 1 + framework/csv/api/catalog/index.ts | 1 + framework/csv/api/catalog/products.ts | 1 + framework/csv/api/checkout/index.ts | 7 + framework/csv/api/index.ts | 42 +++++ framework/csv/api/utils/create-api-handler.ts | 58 ++++++ framework/csv/api/utils/fetch-all-products.ts | 41 +++++ framework/csv/api/utils/fetch-graphql-api.ts | 34 ++++ framework/csv/api/utils/fetch.ts | 2 + framework/csv/api/utils/is-allowed-method.ts | 28 +++ framework/csv/auth/use-login.tsx | 27 +++ framework/csv/auth/use-logout.tsx | 27 +++ framework/csv/auth/use-signup.tsx | 27 +++ framework/csv/cart/index.ts | 4 + framework/csv/cart/use-add-item.tsx | 28 +++ framework/csv/cart/use-cart.tsx | 41 +++++ framework/csv/cart/use-remove-item.tsx | 57 ++++++ framework/csv/cart/use-update-item.tsx | 76 ++++++++ framework/csv/commerce.config.json | 6 + framework/csv/common/get-all-pages.ts | 17 ++ framework/csv/common/get-page.ts | 12 ++ framework/csv/common/get-site-info.ts | 26 +++ framework/csv/common/mock.ts | 33 ++++ framework/csv/common/types.ts | 23 +++ framework/csv/const.ts | 2 + framework/csv/customer/get-customer-id.ts | 5 + framework/csv/customer/index.ts | 1 + framework/csv/customer/use-customer.tsx | 22 +++ framework/csv/index.tsx | 35 ++++ framework/csv/next.config.js | 8 + framework/csv/product/get-all-collections.ts | 17 ++ .../csv/product/get-all-product-paths.ts | 17 ++ framework/csv/product/get-all-products.ts | 26 +++ framework/csv/product/get-product.ts | 25 +++ framework/csv/product/mock.ts | 68 ++++++++ framework/csv/product/use-price.tsx | 2 + framework/csv/product/use-search.tsx | 48 +++++ framework/csv/provider.ts | 21 +++ framework/csv/types.ts | 36 ++++ framework/csv/utils/checkout-create.ts | 5 + framework/csv/utils/checkout-to-cart.ts | 48 +++++ framework/csv/utils/customer-token.ts | 2 + framework/csv/utils/get-categories.ts | 29 +++ framework/csv/utils/get-checkout-id.ts | 5 + framework/csv/utils/get-search-variables.ts | 27 +++ framework/csv/utils/get-sort-variables.ts | 32 ++++ framework/csv/utils/get-vendors.ts | 40 +++++ .../csv/utils/handle-account-activation.ts | 30 ++++ framework/csv/utils/handle-fetch-response.ts | 27 +++ framework/csv/utils/handle-login.ts | 36 ++++ framework/csv/utils/index.ts | 15 ++ framework/csv/utils/normalize.ts | 165 ++++++++++++++++++ framework/csv/utils/throw-user-errors.ts | 38 ++++ framework/csv/wishlist/use-add-item.tsx | 13 ++ framework/csv/wishlist/use-remove-item.tsx | 17 ++ framework/csv/wishlist/use-wishlist.tsx | 46 +++++ tsconfig.json | 4 +- 61 files changed, 1538 insertions(+), 3 deletions(-) create mode 100644 framework/csv/.env.template create mode 100644 framework/csv/README.md create mode 100644 framework/csv/api/cart/index.ts create mode 100644 framework/csv/api/catalog/index.ts create mode 100644 framework/csv/api/catalog/products.ts create mode 100644 framework/csv/api/checkout/index.ts create mode 100644 framework/csv/api/index.ts create mode 100644 framework/csv/api/utils/create-api-handler.ts create mode 100644 framework/csv/api/utils/fetch-all-products.ts create mode 100644 framework/csv/api/utils/fetch-graphql-api.ts create mode 100644 framework/csv/api/utils/fetch.ts create mode 100644 framework/csv/api/utils/is-allowed-method.ts create mode 100644 framework/csv/auth/use-login.tsx create mode 100644 framework/csv/auth/use-logout.tsx create mode 100644 framework/csv/auth/use-signup.tsx create mode 100644 framework/csv/cart/index.ts create mode 100644 framework/csv/cart/use-add-item.tsx create mode 100644 framework/csv/cart/use-cart.tsx create mode 100644 framework/csv/cart/use-remove-item.tsx create mode 100644 framework/csv/cart/use-update-item.tsx create mode 100644 framework/csv/commerce.config.json create mode 100644 framework/csv/common/get-all-pages.ts create mode 100644 framework/csv/common/get-page.ts create mode 100644 framework/csv/common/get-site-info.ts create mode 100644 framework/csv/common/mock.ts create mode 100644 framework/csv/common/types.ts create mode 100644 framework/csv/const.ts create mode 100644 framework/csv/customer/get-customer-id.ts create mode 100644 framework/csv/customer/index.ts create mode 100644 framework/csv/customer/use-customer.tsx create mode 100644 framework/csv/index.tsx create mode 100644 framework/csv/next.config.js create mode 100644 framework/csv/product/get-all-collections.ts create mode 100644 framework/csv/product/get-all-product-paths.ts create mode 100644 framework/csv/product/get-all-products.ts create mode 100644 framework/csv/product/get-product.ts create mode 100644 framework/csv/product/mock.ts create mode 100644 framework/csv/product/use-price.tsx create mode 100644 framework/csv/product/use-search.tsx create mode 100644 framework/csv/provider.ts create mode 100644 framework/csv/types.ts create mode 100644 framework/csv/utils/checkout-create.ts create mode 100644 framework/csv/utils/checkout-to-cart.ts create mode 100644 framework/csv/utils/customer-token.ts create mode 100644 framework/csv/utils/get-categories.ts create mode 100644 framework/csv/utils/get-checkout-id.ts create mode 100644 framework/csv/utils/get-search-variables.ts create mode 100644 framework/csv/utils/get-sort-variables.ts create mode 100644 framework/csv/utils/get-vendors.ts create mode 100644 framework/csv/utils/handle-account-activation.ts create mode 100644 framework/csv/utils/handle-fetch-response.ts create mode 100644 framework/csv/utils/handle-login.ts create mode 100644 framework/csv/utils/index.ts create mode 100644 framework/csv/utils/normalize.ts create mode 100644 framework/csv/utils/throw-user-errors.ts create mode 100644 framework/csv/wishlist/use-add-item.tsx create mode 100644 framework/csv/wishlist/use-remove-item.tsx create mode 100644 framework/csv/wishlist/use-wishlist.tsx diff --git a/.env.template b/.env.template index 9e42e2f31..c4f0e90b9 100644 --- a/.env.template +++ b/.env.template @@ -10,3 +10,5 @@ BIGCOMMERCE_CHANNEL_ID= NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN= NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN= + +NEXT_PUBLIC_CSV_URL= diff --git a/framework/commerce/config.js b/framework/commerce/config.js index 2dd3284bc..5dac1c234 100644 --- a/framework/commerce/config.js +++ b/framework/commerce/config.js @@ -7,7 +7,7 @@ const fs = require('fs') const merge = require('deepmerge') const prettier = require('prettier') -const PROVIDERS = ['bigcommerce', 'shopify'] +const PROVIDERS = ['bigcommerce', 'shopify', 'csv'] function getProviderName() { return ( diff --git a/framework/csv/.env.template b/framework/csv/.env.template new file mode 100644 index 000000000..013ce6364 --- /dev/null +++ b/framework/csv/.env.template @@ -0,0 +1,3 @@ +COMMERCE_PROVIDER=csv + +NEXT_PUBLIC_CSV_URL= diff --git a/framework/csv/README.md b/framework/csv/README.md new file mode 100644 index 000000000..f607bf180 --- /dev/null +++ b/framework/csv/README.md @@ -0,0 +1,3 @@ +## CSV Provider + +_work in progress_ diff --git a/framework/csv/api/cart/index.ts b/framework/csv/api/cart/index.ts new file mode 100644 index 000000000..ea9b101e1 --- /dev/null +++ b/framework/csv/api/cart/index.ts @@ -0,0 +1 @@ +export default function () {} diff --git a/framework/csv/api/catalog/index.ts b/framework/csv/api/catalog/index.ts new file mode 100644 index 000000000..ea9b101e1 --- /dev/null +++ b/framework/csv/api/catalog/index.ts @@ -0,0 +1 @@ +export default function () {} diff --git a/framework/csv/api/catalog/products.ts b/framework/csv/api/catalog/products.ts new file mode 100644 index 000000000..ea9b101e1 --- /dev/null +++ b/framework/csv/api/catalog/products.ts @@ -0,0 +1 @@ +export default function () {} diff --git a/framework/csv/api/checkout/index.ts b/framework/csv/api/checkout/index.ts new file mode 100644 index 000000000..a2b41cda4 --- /dev/null +++ b/framework/csv/api/checkout/index.ts @@ -0,0 +1,7 @@ +import { NextApiRequest, NextApiResponse } from 'next' + +const checkoutApi = async (_req: NextApiRequest, res: NextApiResponse) => { + res.redirect(`https://wa.me/5491141634695`) +} + +export default checkoutApi diff --git a/framework/csv/api/index.ts b/framework/csv/api/index.ts new file mode 100644 index 000000000..aa3fc1fee --- /dev/null +++ b/framework/csv/api/index.ts @@ -0,0 +1,42 @@ +import type { CommerceAPIConfig } from '@commerce/api' + +import { CSV_URL } from '../const' + +if (!CSV_URL) { + throw new Error( + `The environment variable NEXT_PUBLIC_CSV_URL is missing and it's required to access your store` + ) +} + +export interface CSVConfig extends CommerceAPIConfig {} + +export class Config { + private config: CSVConfig + + constructor(config: CSVConfig) { + this.config = config + } + + getConfig(userConfig: Partial = {}) { + return Object.entries(userConfig).reduce( + (cfg, [key, value]) => Object.assign(cfg, { [key]: value }), + { ...this.config } + ) + } + + setConfig(newConfig: Partial) { + Object.assign(this.config, newConfig) + } +} + +const config = new Config({ + locale: 'en-US', +}) + +export function getConfig(userConfig?: Partial) { + return config.getConfig(userConfig) +} + +export function setConfig(newConfig: Partial) { + return config.setConfig(newConfig) +} diff --git a/framework/csv/api/utils/create-api-handler.ts b/framework/csv/api/utils/create-api-handler.ts new file mode 100644 index 000000000..8820aeabc --- /dev/null +++ b/framework/csv/api/utils/create-api-handler.ts @@ -0,0 +1,58 @@ +import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next' +import { ShopifyConfig, getConfig } from '..' + +export type ShopifyApiHandler< + T = any, + H extends ShopifyHandlers = {}, + Options extends {} = {} +> = ( + req: NextApiRequest, + res: NextApiResponse>, + config: ShopifyConfig, + handlers: H, + // Custom configs that may be used by a particular handler + options: Options +) => void | Promise + +export type ShopifyHandler = (options: { + req: NextApiRequest + res: NextApiResponse> + config: ShopifyConfig + body: Body +}) => void | Promise + +export type ShopifyHandlers = { + [k: string]: ShopifyHandler +} + +export type ShopifyApiResponse = { + data: T | null + errors?: { message: string; code?: string }[] +} + +export default function createApiHandler< + T = any, + H extends ShopifyHandlers = {}, + Options extends {} = {} +>( + handler: ShopifyApiHandler, + handlers: H, + defaultOptions: Options +) { + return function getApiHandler({ + config, + operations, + options, + }: { + config?: ShopifyConfig + operations?: Partial + options?: Options extends {} ? Partial : never + } = {}): NextApiHandler { + const ops = { ...operations, ...handlers } + const opts = { ...defaultOptions, ...options } + + return function apiHandler(req, res) { + return handler(req, res, getConfig(config), ops, opts) + } + } +} diff --git a/framework/csv/api/utils/fetch-all-products.ts b/framework/csv/api/utils/fetch-all-products.ts new file mode 100644 index 000000000..9fa70a5ee --- /dev/null +++ b/framework/csv/api/utils/fetch-all-products.ts @@ -0,0 +1,41 @@ +import { ProductEdge } from '../../schema' +import { ShopifyConfig } from '..' + +const fetchAllProducts = async ({ + config, + query, + variables, + acc = [], + cursor, +}: { + config: ShopifyConfig + query: string + acc?: ProductEdge[] + variables?: any + cursor?: string +}): Promise => { + const { data } = await config.fetch(query, { + variables: { ...variables, cursor }, + }) + + const edges: ProductEdge[] = data.products?.edges ?? [] + const hasNextPage = data.products?.pageInfo?.hasNextPage + acc = acc.concat(edges) + + if (hasNextPage) { + const cursor = edges.pop()?.cursor + if (cursor) { + return fetchAllProducts({ + config, + query, + variables, + acc, + cursor, + }) + } + } + + return acc +} + +export default fetchAllProducts diff --git a/framework/csv/api/utils/fetch-graphql-api.ts b/framework/csv/api/utils/fetch-graphql-api.ts new file mode 100644 index 000000000..321cba2aa --- /dev/null +++ b/framework/csv/api/utils/fetch-graphql-api.ts @@ -0,0 +1,34 @@ +import type { GraphQLFetcher } from '@commerce/api' +import fetch from './fetch' + +import { API_URL, API_TOKEN } from '../../const' +import { getError } from '../../utils/handle-fetch-response' + +const fetchGraphqlApi: GraphQLFetcher = async ( + query: string, + { variables } = {}, + fetchOptions +) => { + const res = await fetch(API_URL, { + ...fetchOptions, + method: 'POST', + headers: { + 'X-Shopify-Storefront-Access-Token': API_TOKEN!, + ...fetchOptions?.headers, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query, + variables, + }), + }) + + const { data, errors, status } = await res.json() + + if (errors) { + throw getError(errors, status) + } + + return { data, res } +} +export default fetchGraphqlApi diff --git a/framework/csv/api/utils/fetch.ts b/framework/csv/api/utils/fetch.ts new file mode 100644 index 000000000..0b8367102 --- /dev/null +++ b/framework/csv/api/utils/fetch.ts @@ -0,0 +1,2 @@ +import zeitFetch from '@vercel/fetch' +export default zeitFetch() diff --git a/framework/csv/api/utils/is-allowed-method.ts b/framework/csv/api/utils/is-allowed-method.ts new file mode 100644 index 000000000..78bbba568 --- /dev/null +++ b/framework/csv/api/utils/is-allowed-method.ts @@ -0,0 +1,28 @@ +import type { NextApiRequest, NextApiResponse } from 'next' + +export default function isAllowedMethod( + req: NextApiRequest, + res: NextApiResponse, + allowedMethods: string[] +) { + const methods = allowedMethods.includes('OPTIONS') + ? allowedMethods + : [...allowedMethods, 'OPTIONS'] + + if (!req.method || !methods.includes(req.method)) { + res.status(405) + res.setHeader('Allow', methods.join(', ')) + res.end() + return false + } + + if (req.method === 'OPTIONS') { + res.status(200) + res.setHeader('Allow', methods.join(', ')) + res.setHeader('Content-Length', '0') + res.end() + return false + } + + return true +} diff --git a/framework/csv/auth/use-login.tsx b/framework/csv/auth/use-login.tsx new file mode 100644 index 000000000..da448dcb7 --- /dev/null +++ b/framework/csv/auth/use-login.tsx @@ -0,0 +1,27 @@ +import { useCallback } from 'react' +import type { MutationHook } from '@commerce/utils/types' +import useCustomer from '../customer/use-customer' +import useLogin, { UseLogin } from '@commerce/auth/use-login' + +export default useLogin as UseLogin + +export const handler: MutationHook = { + fetchOptions: { + query: ``, + }, + async fetcher() { + return null + }, + useHook: ({ fetch }) => () => { + const { revalidate } = useCustomer() + + return useCallback( + async function login(input) { + const data = await fetch({ input }) + await revalidate() + return data + }, + [fetch, revalidate] + ) + }, +} diff --git a/framework/csv/auth/use-logout.tsx b/framework/csv/auth/use-logout.tsx new file mode 100644 index 000000000..de8528139 --- /dev/null +++ b/framework/csv/auth/use-logout.tsx @@ -0,0 +1,27 @@ +import { useCallback } from 'react' +import type { MutationHook } from '@commerce/utils/types' +import useLogout, { UseLogout } from '@commerce/auth/use-logout' +import useCustomer from '../customer/use-customer' + +export default useLogout as UseLogout + +export const handler: MutationHook = { + fetchOptions: { + query: ``, + }, + async fetcher() { + return null + }, + useHook: ({ fetch }) => () => { + const { mutate } = useCustomer() + + return useCallback( + async function logout() { + const data = await fetch() + await mutate(null, false) + return data + }, + [fetch, mutate] + ) + }, +} diff --git a/framework/csv/auth/use-signup.tsx b/framework/csv/auth/use-signup.tsx new file mode 100644 index 000000000..db4c7af2b --- /dev/null +++ b/framework/csv/auth/use-signup.tsx @@ -0,0 +1,27 @@ +import { useCallback } from 'react' +import type { MutationHook } from '@commerce/utils/types' +import useSignup, { UseSignup } from '@commerce/auth/use-signup' +import useCustomer from '../customer/use-customer' + +export default useSignup as UseSignup + +export const handler: MutationHook = { + fetchOptions: { + query: ``, + }, + async fetcher() { + return null + }, + useHook: ({ fetch }) => () => { + const { revalidate } = useCustomer() + + return useCallback( + async function signup(input) { + const data = await fetch({ input }) + await revalidate() + return data + }, + [fetch, revalidate] + ) + }, +} diff --git a/framework/csv/cart/index.ts b/framework/csv/cart/index.ts new file mode 100644 index 000000000..f6d36b443 --- /dev/null +++ b/framework/csv/cart/index.ts @@ -0,0 +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' diff --git a/framework/csv/cart/use-add-item.tsx b/framework/csv/cart/use-add-item.tsx new file mode 100644 index 000000000..a18c25d87 --- /dev/null +++ b/framework/csv/cart/use-add-item.tsx @@ -0,0 +1,28 @@ +import { useCallback } from 'react' +import type { MutationHook } from '@commerce/utils/types' +import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item' +import useCart from './use-cart' +import { Cart, CartItemBody } from '../types' + +export default useAddItem as UseAddItem + +export const handler: MutationHook = { + fetchOptions: { + query: ``, + }, + async fetcher() { + return {} as Promise + }, + useHook: ({ fetch }) => () => { + const { mutate } = useCart() + + return useCallback( + async function addItem(input) { + const data = await fetch({ input }) + await mutate(data, false) + return data + }, + [fetch, mutate] + ) + }, +} diff --git a/framework/csv/cart/use-cart.tsx b/framework/csv/cart/use-cart.tsx new file mode 100644 index 000000000..5f466ec14 --- /dev/null +++ b/framework/csv/cart/use-cart.tsx @@ -0,0 +1,41 @@ +import { useMemo } from 'react' +import useCommerceCart, { + FetchCartInput, + UseCart, +} from '@commerce/cart/use-cart' + +import { Cart } from '../types' +import { SWRHook } from '@commerce/utils/types' + +export default useCommerceCart as UseCart + +export const handler: SWRHook< + Cart | null, + {}, + FetchCartInput, + { isEmpty?: boolean } +> = { + fetchOptions: { + query: ``, + }, + async fetcher({ input: { cartId: checkoutId }, options, fetch }) { + return null + }, + useHook: ({ useData }) => (input) => { + const response = useData({ + swrOptions: { revalidateOnFocus: false, ...input?.swrOptions }, + }) + return useMemo( + () => + Object.create(response, { + isEmpty: { + get() { + return (response.data?.lineItems.length ?? 0) <= 0 + }, + enumerable: true, + }, + }), + [response] + ) + }, +} diff --git a/framework/csv/cart/use-remove-item.tsx b/framework/csv/cart/use-remove-item.tsx new file mode 100644 index 000000000..4729bd482 --- /dev/null +++ b/framework/csv/cart/use-remove-item.tsx @@ -0,0 +1,57 @@ +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 { Cart, LineItem } from '../types' + +export type RemoveItemFn = T extends LineItem + ? (input?: RemoveItemInput) => Promise + : (input: RemoveItemInput) => Promise + +export type RemoveItemInput = T extends LineItem + ? Partial + : RemoveItemInputBase + +export default useRemoveItem as UseRemoveItem + +export const handler = { + fetchOptions: { + query: ``, + }, + async fetcher({ input: { itemId } }: HookFetcherContext) { + return { itemId } + }, + useHook: ({ + fetch, + }: MutationHookContext) => < + T extends LineItem | undefined = undefined + >( + ctx: { item?: T } = {} + ) => { + const { item } = ctx + const { mutate } = useCart() + const removeItem: RemoveItemFn = async (input) => { + const itemId = input?.id ?? item?.id + + if (!itemId) { + throw new ValidationError({ + message: 'Invalid input used for this operation', + }) + } + + const data = await fetch({ input: { itemId } }) + await mutate(data, false) + return data + } + + return useCallback(removeItem as RemoveItemFn, [fetch, mutate]) + }, +} diff --git a/framework/csv/cart/use-update-item.tsx b/framework/csv/cart/use-update-item.tsx new file mode 100644 index 000000000..d3eb32c0f --- /dev/null +++ b/framework/csv/cart/use-update-item.tsx @@ -0,0 +1,76 @@ +import { useCallback } from 'react' +import debounce from 'lodash.debounce' +import type { + HookFetcherContext, + MutationHookContext, +} from '@commerce/utils/types' +import { ValidationError } from '@commerce/utils/errors' +import useUpdateItem, { + UpdateItemInput as UpdateItemInputBase, + UseUpdateItem, +} from '@commerce/cart/use-update-item' + +import useCart from './use-cart' +import { handler as removeItemHandler } from './use-remove-item' +import type { Cart, LineItem, UpdateCartItemBody } from '../types' +import { checkoutToCart } from '../utils' + +export type UpdateItemInput = T extends LineItem + ? Partial> + : UpdateItemInputBase + +export default useUpdateItem as UseUpdateItem + +export const handler = { + fetchOptions: { + query: ``, + }, + async fetcher({ + input: { itemId, item }, + }: HookFetcherContext) { + return { + item, + itemId, + } + }, + useHook: ({ + fetch, + }: MutationHookContext) => < + T extends LineItem | undefined = undefined + >( + ctx: { + item?: T + wait?: number + } = {} + ) => { + const { item } = ctx + const { mutate } = useCart() as any + + return useCallback( + debounce(async (input: UpdateItemInput) => { + const itemId = input.id ?? item?.id + const productId = input.productId ?? item?.productId + const variantId = input.productId ?? item?.variantId + if (!itemId || !productId || !variantId) { + throw new ValidationError({ + message: 'Invalid input used for this operation', + }) + } + + const data = await fetch({ + input: { + item: { + productId, + variantId, + quantity: input.quantity, + }, + itemId, + }, + }) + await mutate(data, false) + return data + }, ctx.wait ?? 500), + [fetch, mutate] + ) + }, +} diff --git a/framework/csv/commerce.config.json b/framework/csv/commerce.config.json new file mode 100644 index 000000000..848aeb219 --- /dev/null +++ b/framework/csv/commerce.config.json @@ -0,0 +1,6 @@ +{ + "provider": "csv", + "features": { + "wishlist": false + } +} diff --git a/framework/csv/common/get-all-pages.ts b/framework/csv/common/get-all-pages.ts new file mode 100644 index 000000000..fa1f50f6e --- /dev/null +++ b/framework/csv/common/get-all-pages.ts @@ -0,0 +1,17 @@ +import mock from './mock' +import { Page, CSVConfig } from './types' + +interface GetAllPages { + pages: Page[] +} + +interface Parameters { + config: CSVConfig + preview?: boolean +} + +const getAllPages = async (_parameters: Parameters): Promise => { + return { pages: [mock.page.full] } +} + +export default getAllPages diff --git a/framework/csv/common/get-page.ts b/framework/csv/common/get-page.ts new file mode 100644 index 000000000..ceafded44 --- /dev/null +++ b/framework/csv/common/get-page.ts @@ -0,0 +1,12 @@ +import mock from './mock' +import { Page } from './types' + +export interface GetPage { + page: Page +} + +const getPage = async (): Promise => { + return { page: mock.page.full } +} + +export default getPage diff --git a/framework/csv/common/get-site-info.ts b/framework/csv/common/get-site-info.ts new file mode 100644 index 000000000..571b29c4b --- /dev/null +++ b/framework/csv/common/get-site-info.ts @@ -0,0 +1,26 @@ +import mock from './mock' +import { Brand, Category, CSVConfig } from './types' + +interface GetSiteInfo { + categories: Category[] + brands: { + node: Brand + }[] +} + +interface Parameters { + config: CSVConfig + preview?: boolean +} + +const getSiteInfo = async (_parameters: Parameters): Promise => { + const categories: Category[] = [mock.category.full] + const brands: Brand[] = [mock.brand.full] + + return { + categories, + brands: brands.map((brand) => ({ node: brand })), + } +} + +export default getSiteInfo diff --git a/framework/csv/common/mock.ts b/framework/csv/common/mock.ts new file mode 100644 index 000000000..52a964b8b --- /dev/null +++ b/framework/csv/common/mock.ts @@ -0,0 +1,33 @@ +import { Page, Category, Brand } from './types' + +export default { + page: { + get full(): Page { + return { + id: 'some-page', + body: 'page body', + name: 'page name', + url: '/page-url', + sort_order: 1, + } + }, + }, + category: { + get full(): Category { + return { + entityId: 'category-id', + name: 'Some category', + path: 'some-category-path', + } + }, + }, + brand: { + get full(): Brand { + return { + entityId: 'brand-id', + name: 'Some brand', + path: 'some-brand-path', + } + }, + }, +} diff --git a/framework/csv/common/types.ts b/framework/csv/common/types.ts new file mode 100644 index 000000000..248a61d8b --- /dev/null +++ b/framework/csv/common/types.ts @@ -0,0 +1,23 @@ +import { CommerceConfig } from '@commerce' + +export type CSVConfig = Partial + +export interface Page { + id: string + name: string + url: string + sort_order?: number + body: string +} + +export interface Category { + entityId: string + name: string + path: string +} + +export interface Brand { + entityId: string + name: string + path: string +} diff --git a/framework/csv/const.ts b/framework/csv/const.ts new file mode 100644 index 000000000..547442647 --- /dev/null +++ b/framework/csv/const.ts @@ -0,0 +1,2 @@ +export const CSV_URL = process.env.NEXT_PUBLIC_CSV_URL +export const API_URL = '/' diff --git a/framework/csv/customer/get-customer-id.ts b/framework/csv/customer/get-customer-id.ts new file mode 100644 index 000000000..994ebd5bb --- /dev/null +++ b/framework/csv/customer/get-customer-id.ts @@ -0,0 +1,5 @@ +async function getCustomerId(): Promise { + return undefined +} + +export default getCustomerId diff --git a/framework/csv/customer/index.ts b/framework/csv/customer/index.ts new file mode 100644 index 000000000..6c903ecc5 --- /dev/null +++ b/framework/csv/customer/index.ts @@ -0,0 +1 @@ +export { default as useCustomer } from './use-customer' diff --git a/framework/csv/customer/use-customer.tsx b/framework/csv/customer/use-customer.tsx new file mode 100644 index 000000000..2f51297a8 --- /dev/null +++ b/framework/csv/customer/use-customer.tsx @@ -0,0 +1,22 @@ +import useCustomer, { UseCustomer } from '@commerce/customer/use-customer' +import { Customer } from '@commerce/types' +import { SWRHook } from '@commerce/utils/types' + +export default useCustomer as UseCustomer + +export const handler: SWRHook = { + fetchOptions: { + query: ``, + }, + async fetcher() { + return null + }, + useHook: ({ useData }) => (input) => { + return useData({ + swrOptions: { + revalidateOnFocus: false, + ...input?.swrOptions, + }, + }) + }, +} diff --git a/framework/csv/index.tsx b/framework/csv/index.tsx new file mode 100644 index 000000000..c5d636f89 --- /dev/null +++ b/framework/csv/index.tsx @@ -0,0 +1,35 @@ +import * as React from 'react' + +import { + CommerceConfig, + CommerceProvider as CoreCommerceProvider, + useCommerce as useCoreCommerce, +} from '@commerce' + +import { csvProvider, CSVProvider } from './provider' +import { CSVConfig } from './common/types' + +export { csvProvider } +export type { CSVProvider } + +export const csvConfig: CommerceConfig = { + locale: 'en-us', +} + +export type CSVProps = { + children?: React.ReactNode + locale: string +} & CSVConfig + +export function CommerceProvider({ children, ...config }: CSVProps) { + return ( + + {children} + + ) +} + +export const useCommerce = () => useCoreCommerce() diff --git a/framework/csv/next.config.js b/framework/csv/next.config.js new file mode 100644 index 000000000..718fa7d13 --- /dev/null +++ b/framework/csv/next.config.js @@ -0,0 +1,8 @@ +const commerce = require('./commerce.config.json') + +module.exports = { + commerce, + images: { + domains: ['placehold.it', 'picsum.photos'], + }, +} diff --git a/framework/csv/product/get-all-collections.ts b/framework/csv/product/get-all-collections.ts new file mode 100644 index 000000000..503347c46 --- /dev/null +++ b/framework/csv/product/get-all-collections.ts @@ -0,0 +1,17 @@ +interface CollectionEdge { + entityId: string + name: string + path: string +} + +interface GetAllCollections { + categories: CollectionEdge[] +} + +const getAllCollections = async (): Promise => { + return { + categories: [], + } +} + +export default getAllCollections diff --git a/framework/csv/product/get-all-product-paths.ts b/framework/csv/product/get-all-product-paths.ts new file mode 100644 index 000000000..ec95905a3 --- /dev/null +++ b/framework/csv/product/get-all-product-paths.ts @@ -0,0 +1,17 @@ +interface ProductPath { + node: { + path: string + } +} + +interface GetAllProductPaths { + products: ProductPath[] +} + +const getAllProductPaths = async (): Promise => { + return { + products: [], + } +} + +export default getAllProductPaths diff --git a/framework/csv/product/get-all-products.ts b/framework/csv/product/get-all-products.ts new file mode 100644 index 000000000..67e410d7d --- /dev/null +++ b/framework/csv/product/get-all-products.ts @@ -0,0 +1,26 @@ +import { Product } from '@commerce/types' +import { CSVConfig } from '../index' + +import mock from './mock' + +interface GetAllProducts { + products: Product[] +} + +interface Parameters { + variables: { + first: number + } + config: CSVConfig + preview?: boolean +} + +const getAllProducts = async ( + _parameters: Parameters +): Promise => { + return { + products: [mock.full], + } +} + +export default getAllProducts diff --git a/framework/csv/product/get-product.ts b/framework/csv/product/get-product.ts new file mode 100644 index 000000000..319b94fbb --- /dev/null +++ b/framework/csv/product/get-product.ts @@ -0,0 +1,25 @@ +import { Product } from '@commerce/types' + +import { CSVConfig } from '../index' + +import mock from './mock' + +interface GetProduct { + product: Product | null +} + +interface Parameters { + variables: { + slug?: string + } + config: CSVConfig + preview?: boolean +} + +const getProduct = async (_parameters: Parameters): Promise => { + return { + product: mock.full, + } +} + +export default getProduct diff --git a/framework/csv/product/mock.ts b/framework/csv/product/mock.ts new file mode 100644 index 000000000..fee35fedd --- /dev/null +++ b/framework/csv/product/mock.ts @@ -0,0 +1,68 @@ +import { Product } from '@commerce/types' + +export default { + get full(): Product { + return { + id: 'some-product', + name: 'Some product', + description: 'Some description', + descriptionHtml: `

Some HTML description

`, + slug: 'some-product', + path: '/product', + images: [ + { + url: 'https://picsum.photos/640/480', + alt: 'placeholder', + }, + { + url: 'https://picsum.photos/640/480', + alt: 'placeholder', + }, + ], + variants: [ + { + id: 'variant-1', + options: [ + { + id: 'product-1', + displayName: 'Product', + values: [ + { + label: 'Product color 1', + hexColors: ['#333', '#121'], + }, + ], + }, + ], + }, + ], + price: { + currencyCode: 'ARS', + value: 100, + }, + options: [ + { + id: 'option-1', + displayName: 'Option 1', + values: [ + { + label: 'Product color 1', + hexColors: ['#333', '#121'], + }, + ], + }, + { + id: 'option-2', + displayName: 'Option 2', + values: [ + { + label: 'Product color 2', + hexColors: ['#FF0000'], + }, + ], + }, + ], + sku: 'sku-product', + } + }, +} diff --git a/framework/csv/product/use-price.tsx b/framework/csv/product/use-price.tsx new file mode 100644 index 000000000..0174faf5e --- /dev/null +++ b/framework/csv/product/use-price.tsx @@ -0,0 +1,2 @@ +export * from '@commerce/product/use-price' +export { default } from '@commerce/product/use-price' diff --git a/framework/csv/product/use-search.tsx b/framework/csv/product/use-search.tsx new file mode 100644 index 000000000..057fefe4e --- /dev/null +++ b/framework/csv/product/use-search.tsx @@ -0,0 +1,48 @@ +import { SWRHook } from '@commerce/utils/types' +import useSearch, { UseSearch } from '@commerce/product/use-search' + +import { Product } from '@commerce/types' + +export default useSearch as UseSearch + +export type SearchProductsInput = { + search?: string + categoryId?: string + brandId?: string + sort?: string +} + +export type SearchProductsData = { + products: Product[] + found: boolean +} + +export const handler: SWRHook< + SearchProductsData, + SearchProductsInput, + SearchProductsInput +> = { + fetchOptions: { + query: ``, + }, + async fetcher() { + return { + products: [], + found: false, + } + }, + useHook: ({ useData }) => (input = {}) => { + return useData({ + input: [ + ['search', input.search], + ['categoryId', input.categoryId], + ['brandId', input.brandId], + ['sort', input.sort], + ], + swrOptions: { + revalidateOnFocus: false, + ...input.swrOptions, + }, + }) + }, +} diff --git a/framework/csv/provider.ts b/framework/csv/provider.ts new file mode 100644 index 000000000..a2fc3afb0 --- /dev/null +++ b/framework/csv/provider.ts @@ -0,0 +1,21 @@ +import { handler as useCart } from './cart/use-cart' +import { handler as useAddItem } from './cart/use-add-item' +import { handler as useUpdateItem } from './cart/use-update-item' +import { handler as useRemoveItem } from './cart/use-remove-item' + +import { handler as useCustomer } from './customer/use-customer' +import { handler as useSearch } from './product/use-search' + +import { handler as useLogin } from './auth/use-login' +import { handler as useLogout } from './auth/use-logout' +import { handler as useSignup } from './auth/use-signup' + +export const csvProvider = { + locale: 'en-us', + cart: { useCart, useAddItem, useUpdateItem, useRemoveItem }, + customer: { useCustomer }, + products: { useSearch }, + auth: { useLogin, useLogout, useSignup }, +} + +export type CSVProvider = typeof csvProvider diff --git a/framework/csv/types.ts b/framework/csv/types.ts new file mode 100644 index 000000000..c0136b3f5 --- /dev/null +++ b/framework/csv/types.ts @@ -0,0 +1,36 @@ +import * as Core from '@commerce/types' + +export type Cart = Core.Cart & { + lineItems: LineItem[] +} +export interface LineItem extends Core.LineItem { + options?: any[] +} + +/** + * Cart mutations + */ + +export type OptionSelections = { + option_id: number + option_value: number | string +} + +export type CartItemBody = Core.CartItemBody & { + productId: string // The product id is always required for BC + optionSelections?: OptionSelections +} + +export type GetCartHandlerBody = Core.GetCartHandlerBody + +export type AddCartItemBody = Core.AddCartItemBody + +export type AddCartItemHandlerBody = Core.AddCartItemHandlerBody + +export type UpdateCartItemBody = Core.UpdateCartItemBody + +export type UpdateCartItemHandlerBody = Core.UpdateCartItemHandlerBody + +export type RemoveCartItemBody = Core.RemoveCartItemBody + +export type RemoveCartItemHandlerBody = Core.RemoveCartItemHandlerBody diff --git a/framework/csv/utils/checkout-create.ts b/framework/csv/utils/checkout-create.ts new file mode 100644 index 000000000..091ea8d51 --- /dev/null +++ b/framework/csv/utils/checkout-create.ts @@ -0,0 +1,5 @@ +export const checkoutCreate = async (): Promise => { + return Promise.resolve() +} + +export default checkoutCreate diff --git a/framework/csv/utils/checkout-to-cart.ts b/framework/csv/utils/checkout-to-cart.ts new file mode 100644 index 000000000..034ff11d7 --- /dev/null +++ b/framework/csv/utils/checkout-to-cart.ts @@ -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 +} + +export type CheckoutPayload = + | CheckoutLineItemsAddPayload + | CheckoutLineItemsUpdatePayload + | CheckoutLineItemsRemovePayload + | CheckoutCreatePayload + | CheckoutQuery + +const checkoutToCart = (checkoutPayload?: Maybe): 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 diff --git a/framework/csv/utils/customer-token.ts b/framework/csv/utils/customer-token.ts new file mode 100644 index 000000000..a5a860038 --- /dev/null +++ b/framework/csv/utils/customer-token.ts @@ -0,0 +1,2 @@ +export const getCustomerToken = () => null +export const setCustomerToken = () => null diff --git a/framework/csv/utils/get-categories.ts b/framework/csv/utils/get-categories.ts new file mode 100644 index 000000000..cce4b2ad7 --- /dev/null +++ b/framework/csv/utils/get-categories.ts @@ -0,0 +1,29 @@ +import { ShopifyConfig } from '../api' +import { CollectionEdge } from '../schema' +import getSiteCollectionsQuery from './queries/get-all-collections-query' + +export type Category = { + entityId: string + name: string + path: string +} + +const getCategories = async (config: ShopifyConfig): Promise => { + const { data } = await config.fetch(getSiteCollectionsQuery, { + variables: { + first: 250, + }, + }) + + return ( + data.collections?.edges?.map( + ({ node: { id: entityId, title: name, handle } }: CollectionEdge) => ({ + entityId, + name, + path: `/${handle}`, + }) + ) ?? [] + ) +} + +export default getCategories diff --git a/framework/csv/utils/get-checkout-id.ts b/framework/csv/utils/get-checkout-id.ts new file mode 100644 index 000000000..05452c948 --- /dev/null +++ b/framework/csv/utils/get-checkout-id.ts @@ -0,0 +1,5 @@ +const getCheckoutId = (id?: string) => { + return id +} + +export default getCheckoutId diff --git a/framework/csv/utils/get-search-variables.ts b/framework/csv/utils/get-search-variables.ts new file mode 100644 index 000000000..c1b40ae5d --- /dev/null +++ b/framework/csv/utils/get-search-variables.ts @@ -0,0 +1,27 @@ +import getSortVariables from './get-sort-variables' +import type { SearchProductsInput } from '../product/use-search' + +export const getSearchVariables = ({ + brandId, + search, + categoryId, + sort, +}: SearchProductsInput) => { + let query = '' + + if (search) { + query += `product_type:${search} OR title:${search} OR tag:${search}` + } + + if (brandId) { + query += `${search ? ' AND ' : ''}vendor:${brandId}` + } + + return { + categoryId, + query, + ...getSortVariables(sort, !!categoryId), + } +} + +export default getSearchVariables diff --git a/framework/csv/utils/get-sort-variables.ts b/framework/csv/utils/get-sort-variables.ts new file mode 100644 index 000000000..141d9a180 --- /dev/null +++ b/framework/csv/utils/get-sort-variables.ts @@ -0,0 +1,32 @@ +const getSortVariables = (sort?: string, isCategory: boolean = false) => { + let output = {} + switch (sort) { + case 'price-asc': + output = { + sortKey: 'PRICE', + reverse: false, + } + break + case 'price-desc': + output = { + sortKey: 'PRICE', + reverse: true, + } + break + case 'trending-desc': + output = { + sortKey: 'BEST_SELLING', + reverse: false, + } + break + case 'latest-desc': + output = { + sortKey: isCategory ? 'CREATED' : 'CREATED_AT', + reverse: true, + } + break + } + return output +} + +export default getSortVariables diff --git a/framework/csv/utils/get-vendors.ts b/framework/csv/utils/get-vendors.ts new file mode 100644 index 000000000..24843f177 --- /dev/null +++ b/framework/csv/utils/get-vendors.ts @@ -0,0 +1,40 @@ +import { ShopifyConfig } from '../api' +import fetchAllProducts from '../api/utils/fetch-all-products' +import getAllProductVendors from './queries/get-all-product-vendors-query' + +export type Brand = { + entityId: string + name: string + path: string +} + +export type BrandEdge = { + node: Brand +} + +export type Brands = BrandEdge[] + +const getVendors = async (config: ShopifyConfig): Promise => { + const vendors = await fetchAllProducts({ + config, + query: getAllProductVendors, + variables: { + first: 250, + }, + }) + + let vendorsStrings = vendors.map(({ node: { vendor } }) => vendor) + + 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 diff --git a/framework/csv/utils/handle-account-activation.ts b/framework/csv/utils/handle-account-activation.ts new file mode 100644 index 000000000..d11f80ba1 --- /dev/null +++ b/framework/csv/utils/handle-account-activation.ts @@ -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: (options: FetcherOptions) => Promise, + input: MutationCustomerActivateByUrlArgs +) => { + try { + const { customerActivateByUrl } = await fetch< + Mutation, + MutationCustomerActivateArgs + >({ + query: customerActivateByUrlMutation, + variables: { + input, + }, + }) + + throwUserErrors(customerActivateByUrl?.customerUserErrors) + } catch (error) {} +} + +export default handleAccountActivation diff --git a/framework/csv/utils/handle-fetch-response.ts b/framework/csv/utils/handle-fetch-response.ts new file mode 100644 index 000000000..8d7427d91 --- /dev/null +++ b/framework/csv/utils/handle-fetch-response.ts @@ -0,0 +1,27 @@ +import { FetcherError } from '@commerce/utils/errors' + +export function getError(errors: any[], status: number) { + errors = errors ?? [{ message: 'Failed to fetch Shopify API' }] + return new FetcherError({ errors, status }) +} + +export async function getAsyncError(res: Response) { + const data = await res.json() + return getError(data.errors, res.status) +} + +const handleFetchResponse = async (res: Response) => { + if (res.ok) { + const { data, errors } = await res.json() + + if (errors && errors.length) { + throw getError(errors, res.status) + } + + return data + } + + throw await getAsyncError(res) +} + +export default handleFetchResponse diff --git a/framework/csv/utils/handle-login.ts b/framework/csv/utils/handle-login.ts new file mode 100644 index 000000000..de86fa1d2 --- /dev/null +++ b/framework/csv/utils/handle-login.ts @@ -0,0 +1,36 @@ +import { FetcherOptions } from '@commerce/utils/types' +import { CustomerAccessTokenCreateInput } from '../schema' +import { setCustomerToken } from './customer-token' +import { customerAccessTokenCreateMutation } from './mutations' +import throwUserErrors from './throw-user-errors' + +const handleLogin = (data: any) => { + const response = data.customerAccessTokenCreate + throwUserErrors(response?.customerUserErrors) + + const customerAccessToken = response?.customerAccessToken + const accessToken = customerAccessToken?.accessToken + + if (accessToken) { + setCustomerToken(accessToken) + } + + return customerAccessToken +} + +export const handleAutomaticLogin = async ( + fetch: (options: FetcherOptions) => Promise, + input: CustomerAccessTokenCreateInput +) => { + try { + const loginData = await fetch({ + query: customerAccessTokenCreateMutation, + variables: { + input, + }, + }) + handleLogin(loginData) + } catch (error) {} +} + +export default handleLogin diff --git a/framework/csv/utils/index.ts b/framework/csv/utils/index.ts new file mode 100644 index 000000000..61e5975d7 --- /dev/null +++ b/framework/csv/utils/index.ts @@ -0,0 +1,15 @@ +export { default as handleFetchResponse } from './handle-fetch-response' +export { default as getSearchVariables } from './get-search-variables' +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' +export * from './customer-token' diff --git a/framework/csv/utils/normalize.ts b/framework/csv/utils/normalize.ts new file mode 100644 index 000000000..4ebc3a1ae --- /dev/null +++ b/framework/csv/utils/normalize.ts @@ -0,0 +1,165 @@ +import { Product } from '@commerce/types' + +import { + Product as ShopifyProduct, + Checkout, + CheckoutLineItemEdge, + SelectedOption, + ImageConnection, + ProductVariantConnection, + MoneyV2, + ProductOption, +} from '../schema' + +import type { Cart, LineItem } from '../types' + +const money = ({ amount, currencyCode }: MoneyV2) => { + return { + value: +amount, + currencyCode, + } +} + +const normalizeProductOption = ({ + id, + name: displayName, + values, +}: ProductOption) => { + return { + __typename: 'MultipleChoiceOption', + id, + displayName, + values: values.map((value) => { + let output: any = { + label: value, + } + if (displayName.match(/colou?r/gi)) { + output = { + ...output, + hexColors: [value], + } + } + return output + }), + } +} + +const normalizeProductImages = ({ edges }: ImageConnection) => + edges?.map(({ node: { originalSrc: url, ...rest } }) => ({ + url, + ...rest, + })) + +const normalizeProductVariants = ({ edges }: ProductVariantConnection) => { + return edges?.map( + ({ + node: { id, selectedOptions, sku, title, priceV2, compareAtPriceV2 }, + }) => { + 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 + }), + } + } + ) +} + +export function normalizeProduct(productNode: ShopifyProduct): Product { + const { + id, + title: name, + vendor, + images, + variants, + description, + descriptionHtml, + handle, + priceRange, + options, + ...rest + } = productNode + + const product = { + id, + name, + vendor, + path: `/${handle}`, + slug: handle?.replace(/^\/+|\/+$/g, ''), + price: money(priceRange?.minVariantPrice), + images: normalizeProductImages(images), + variants: variants ? normalizeProductVariants(variants) : [], + options: options + ? options + .filter((o) => o.name !== 'Title') // By default Shopify adds a 'Title' name when there's only one option. We don't need it. https://community.shopify.com/c/Shopify-APIs-SDKs/Adding-new-product-variant-is-automatically-adding-quot-Default/td-p/358095 + .map((o) => normalizeProductOption(o)) + : [], + ...(description && { description }), + ...(descriptionHtml && { descriptionHtml }), + ...rest, + } + + return product +} + +export function normalizeCart(checkout: Checkout): Cart { + return { + id: checkout.id, + customerId: '', + email: '', + createdAt: checkout.createdAt, + currency: { + code: checkout.totalPriceV2?.currencyCode, + }, + taxesIncluded: checkout.taxesIncluded, + lineItems: checkout.lineItems?.edges.map(normalizeLineItem), + lineItemsSubtotalPrice: +checkout.subtotalPriceV2?.amount, + subtotalPrice: +checkout.subtotalPriceV2?.amount, + totalPrice: checkout.totalPriceV2?.amount, + discounts: [], + } +} + +function normalizeLineItem({ + node: { id, title, variant, quantity, ...rest }, +}: CheckoutLineItemEdge): LineItem { + return { + id, + variantId: String(variant?.id), + productId: String(variant?.id), + name: `${title}`, + quantity, + variant: { + id: String(variant?.id), + sku: variant?.sku ?? '', + name: variant?.title!, + image: { + url: variant?.image?.originalSrc ?? '/product-img-placeholder.svg', + }, + requiresShipping: variant?.requiresShipping ?? false, + price: variant?.priceV2?.amount, + listPrice: variant?.compareAtPriceV2?.amount, + }, + path: String(variant?.product?.handle), + discounts: [], + options: + // By default Shopify adds a default variant with default names, we're removing it. https://community.shopify.com/c/Shopify-APIs-SDKs/Adding-new-product-variant-is-automatically-adding-quot-Default/td-p/358095 + variant?.title == 'Default Title' + ? [] + : [ + { + value: variant?.title, + }, + ], + } +} diff --git a/framework/csv/utils/throw-user-errors.ts b/framework/csv/utils/throw-user-errors.ts new file mode 100644 index 000000000..5488ba282 --- /dev/null +++ b/framework/csv/utils/throw-user-errors.ts @@ -0,0 +1,38 @@ +import { ValidationError } from '@commerce/utils/errors' + +import { + CheckoutErrorCode, + CheckoutUserError, + CustomerErrorCode, + CustomerUserError, +} from '../schema' + +export type UserErrors = Array + +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 diff --git a/framework/csv/wishlist/use-add-item.tsx b/framework/csv/wishlist/use-add-item.tsx new file mode 100644 index 000000000..75f067c3a --- /dev/null +++ b/framework/csv/wishlist/use-add-item.tsx @@ -0,0 +1,13 @@ +import { useCallback } from 'react' + +export function emptyHook() { + const useEmptyHook = async (options = {}) => { + return useCallback(async function () { + return Promise.resolve() + }, []) + } + + return useEmptyHook +} + +export default emptyHook diff --git a/framework/csv/wishlist/use-remove-item.tsx b/framework/csv/wishlist/use-remove-item.tsx new file mode 100644 index 000000000..a2d3a8a05 --- /dev/null +++ b/framework/csv/wishlist/use-remove-item.tsx @@ -0,0 +1,17 @@ +import { useCallback } from 'react' + +type Options = { + includeProducts?: boolean +} + +export function emptyHook(options?: Options) { + const useEmptyHook = async ({ id }: { id: string | number }) => { + return useCallback(async function () { + return Promise.resolve() + }, []) + } + + return useEmptyHook +} + +export default emptyHook diff --git a/framework/csv/wishlist/use-wishlist.tsx b/framework/csv/wishlist/use-wishlist.tsx new file mode 100644 index 000000000..8c44847e0 --- /dev/null +++ b/framework/csv/wishlist/use-wishlist.tsx @@ -0,0 +1,46 @@ +// TODO: replace this hook and other wishlist hooks with a handler, or remove them if +// Shopify doesn't have a wishlist + +import { Product } from '@commerce/types' +import { HookFetcher } from '@commerce/utils/types' + +const defaultOpts = {} + +export type Wishlist = { + items: [ + { + product_id: number + variant_id: number + id: number + product: Product + } + ] +} + +export interface UseWishlistOptions { + includeProducts?: boolean +} + +export interface UseWishlistInput extends UseWishlistOptions { + customerId?: number +} + +export const fetcher: HookFetcher = () => { + return null +} + +export function extendHook( + customFetcher: typeof fetcher, + // swrOptions?: SwrOptions + swrOptions?: any +) { + const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => { + return { data: null } + } + + useWishlist.extend = extendHook + + return useWishlist +} + +export default extendHook(fetcher) diff --git a/tsconfig.json b/tsconfig.json index e20f37099..cd26826b5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,8 +22,8 @@ "@components/*": ["components/*"], "@commerce": ["framework/commerce"], "@commerce/*": ["framework/commerce/*"], - "@framework": ["framework/shopify"], - "@framework/*": ["framework/shopify/*"] + "@framework": ["framework/csv"], + "@framework/*": ["framework/csv/*"] } }, "include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],