From 244ad904f7e5c858937d248372b18a2e3fdc0627 Mon Sep 17 00:00:00 2001 From: rafik mrabet Date: Wed, 30 Mar 2022 18:33:32 +0100 Subject: [PATCH] added appibase provider --- packages/appibase/.env.template | 1 + packages/appibase/.prettierignore | 2 + packages/appibase/.prettierrc | 6 + packages/appibase/README.md | 1 + packages/appibase/package-lock.json | 18 ++ packages/appibase/package.json | 84 +++++++ .../appibase/src/api/endpoints/cart/index.ts | 1 + .../src/api/endpoints/catalog/index.ts | 1 + .../src/api/endpoints/catalog/products.ts | 1 + .../src/api/endpoints/checkout/index.ts | 1 + .../src/api/endpoints/customer/address.ts | 1 + .../src/api/endpoints/customer/card.ts | 1 + .../src/api/endpoints/customer/index.ts | 1 + .../appibase/src/api/endpoints/login/index.ts | 1 + .../src/api/endpoints/logout/index.ts | 1 + .../src/api/endpoints/signup/index.ts | 1 + .../src/api/endpoints/wishlist/index.tsx | 1 + packages/appibase/src/api/index.ts | 43 ++++ .../src/api/operations/get-all-pages.ts | 21 ++ .../api/operations/get-all-product-paths.ts | 17 ++ .../src/api/operations/get-all-products.ts | 33 +++ .../api/operations/get-customer-wishlist.ts | 6 + .../appibase/src/api/operations/get-page.ts | 15 ++ .../src/api/operations/get-product.ts | 29 +++ .../src/api/operations/get-site-info.ts | 38 +++ packages/appibase/src/api/operations/index.ts | 6 + .../appibase/src/api/utils/access-token.ts | 35 +++ .../appibase/src/api/utils/fetch-appibase.ts | 28 +++ packages/appibase/src/api/utils/fetch.ts | 3 + packages/appibase/src/api/utils/normalize.ts | 82 ++++++ packages/appibase/src/auth/index.ts | 3 + packages/appibase/src/auth/use-login.tsx | 16 ++ packages/appibase/src/auth/use-logout.tsx | 17 ++ packages/appibase/src/auth/use-signup.tsx | 19 ++ packages/appibase/src/cart/index.ts | 4 + packages/appibase/src/cart/use-add-item.tsx | 39 +++ packages/appibase/src/cart/use-cart.tsx | 51 ++++ .../appibase/src/cart/use-remove-item.tsx | 40 +++ .../appibase/src/cart/use-update-item.tsx | 45 ++++ .../appibase/src/checkout/use-checkout.tsx | 16 ++ packages/appibase/src/commerce.config.json | 10 + packages/appibase/src/const.ts | 7 + .../src/customer/address/use-add-item.tsx | 17 ++ .../src/customer/card/use-add-item.tsx | 17 ++ packages/appibase/src/customer/index.ts | 1 + .../appibase/src/customer/use-customer.tsx | 17 ++ packages/appibase/src/data.json | 235 ++++++++++++++++++ packages/appibase/src/fetcher.ts | 25 ++ packages/appibase/src/index.tsx | 12 + packages/appibase/src/next.config.cjs | 8 + packages/appibase/src/product/index.ts | 2 + packages/appibase/src/product/use-price.tsx | 2 + packages/appibase/src/product/use-search.tsx | 54 ++++ packages/appibase/src/provider.ts | 22 ++ packages/appibase/src/types.ts | 132 ++++++++++ .../src/utils/handle-fetch-response.ts | 27 ++ packages/appibase/src/utils/index.ts | 1 + .../appibase/src/wishlist/use-add-item.tsx | 13 + .../appibase/src/wishlist/use-remove-item.tsx | 17 ++ .../appibase/src/wishlist/use-wishlist.tsx | 43 ++++ packages/appibase/taskfile.js | 20 ++ packages/appibase/tsconfig.json | 21 ++ site/commerce-config.js | 1 + site/commerce.config.json | 2 +- site/tsconfig.json | 4 +- yarn.lock | 60 +++++ 66 files changed, 1496 insertions(+), 3 deletions(-) create mode 100644 packages/appibase/.env.template create mode 100644 packages/appibase/.prettierignore create mode 100644 packages/appibase/.prettierrc create mode 100644 packages/appibase/README.md create mode 100644 packages/appibase/package-lock.json create mode 100644 packages/appibase/package.json create mode 100644 packages/appibase/src/api/endpoints/cart/index.ts create mode 100644 packages/appibase/src/api/endpoints/catalog/index.ts create mode 100644 packages/appibase/src/api/endpoints/catalog/products.ts create mode 100644 packages/appibase/src/api/endpoints/checkout/index.ts create mode 100644 packages/appibase/src/api/endpoints/customer/address.ts create mode 100644 packages/appibase/src/api/endpoints/customer/card.ts create mode 100644 packages/appibase/src/api/endpoints/customer/index.ts create mode 100644 packages/appibase/src/api/endpoints/login/index.ts create mode 100644 packages/appibase/src/api/endpoints/logout/index.ts create mode 100644 packages/appibase/src/api/endpoints/signup/index.ts create mode 100644 packages/appibase/src/api/endpoints/wishlist/index.tsx create mode 100644 packages/appibase/src/api/index.ts create mode 100644 packages/appibase/src/api/operations/get-all-pages.ts create mode 100644 packages/appibase/src/api/operations/get-all-product-paths.ts create mode 100644 packages/appibase/src/api/operations/get-all-products.ts create mode 100644 packages/appibase/src/api/operations/get-customer-wishlist.ts create mode 100644 packages/appibase/src/api/operations/get-page.ts create mode 100644 packages/appibase/src/api/operations/get-product.ts create mode 100644 packages/appibase/src/api/operations/get-site-info.ts create mode 100644 packages/appibase/src/api/operations/index.ts create mode 100644 packages/appibase/src/api/utils/access-token.ts create mode 100644 packages/appibase/src/api/utils/fetch-appibase.ts create mode 100644 packages/appibase/src/api/utils/fetch.ts create mode 100644 packages/appibase/src/api/utils/normalize.ts create mode 100644 packages/appibase/src/auth/index.ts create mode 100644 packages/appibase/src/auth/use-login.tsx create mode 100644 packages/appibase/src/auth/use-logout.tsx create mode 100644 packages/appibase/src/auth/use-signup.tsx create mode 100644 packages/appibase/src/cart/index.ts create mode 100644 packages/appibase/src/cart/use-add-item.tsx create mode 100644 packages/appibase/src/cart/use-cart.tsx create mode 100644 packages/appibase/src/cart/use-remove-item.tsx create mode 100644 packages/appibase/src/cart/use-update-item.tsx create mode 100644 packages/appibase/src/checkout/use-checkout.tsx create mode 100644 packages/appibase/src/commerce.config.json create mode 100644 packages/appibase/src/const.ts create mode 100644 packages/appibase/src/customer/address/use-add-item.tsx create mode 100644 packages/appibase/src/customer/card/use-add-item.tsx create mode 100644 packages/appibase/src/customer/index.ts create mode 100644 packages/appibase/src/customer/use-customer.tsx create mode 100644 packages/appibase/src/data.json create mode 100644 packages/appibase/src/fetcher.ts create mode 100644 packages/appibase/src/index.tsx create mode 100644 packages/appibase/src/next.config.cjs create mode 100644 packages/appibase/src/product/index.ts create mode 100644 packages/appibase/src/product/use-price.tsx create mode 100644 packages/appibase/src/product/use-search.tsx create mode 100644 packages/appibase/src/provider.ts create mode 100644 packages/appibase/src/types.ts create mode 100644 packages/appibase/src/utils/handle-fetch-response.ts create mode 100644 packages/appibase/src/utils/index.ts create mode 100644 packages/appibase/src/wishlist/use-add-item.tsx create mode 100644 packages/appibase/src/wishlist/use-remove-item.tsx create mode 100644 packages/appibase/src/wishlist/use-wishlist.tsx create mode 100644 packages/appibase/taskfile.js create mode 100644 packages/appibase/tsconfig.json diff --git a/packages/appibase/.env.template b/packages/appibase/.env.template new file mode 100644 index 000000000..0ec47188b --- /dev/null +++ b/packages/appibase/.env.template @@ -0,0 +1 @@ +COMMERCE_PROVIDER=appibase \ No newline at end of file diff --git a/packages/appibase/.prettierignore b/packages/appibase/.prettierignore new file mode 100644 index 000000000..f06235c46 --- /dev/null +++ b/packages/appibase/.prettierignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/packages/appibase/.prettierrc b/packages/appibase/.prettierrc new file mode 100644 index 000000000..e1076edfa --- /dev/null +++ b/packages/appibase/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "useTabs": false +} diff --git a/packages/appibase/README.md b/packages/appibase/README.md new file mode 100644 index 000000000..a3bc1db32 --- /dev/null +++ b/packages/appibase/README.md @@ -0,0 +1 @@ +# Next.js Local Provider diff --git a/packages/appibase/package-lock.json b/packages/appibase/package-lock.json new file mode 100644 index 000000000..9bbc15602 --- /dev/null +++ b/packages/appibase/package-lock.json @@ -0,0 +1,18 @@ +{ + "name": "appibase", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "js-cookies": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/js-cookies/-/js-cookies-1.0.4.tgz", + "integrity": "sha1-1G5XbEIP9tVULA9SttTvfWN+dU4=" + }, + "urijs": { + "version": "1.19.10", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.10.tgz", + "integrity": "sha512-EzauQlgKuJgsXOqoMrCiePBf4At5jVqRhXykF3Wfb8ZsOBMxPcfiVBcsHXug4Aepb/ICm2PIgqAUGMelgdrWEg==" + } + } +} diff --git a/packages/appibase/package.json b/packages/appibase/package.json new file mode 100644 index 000000000..b070d793c --- /dev/null +++ b/packages/appibase/package.json @@ -0,0 +1,84 @@ +{ + "name": "appibase", + "version": "0.0.1", + "license": "MIT", + "scripts": { + "release": "taskr release", + "build": "taskr build", + "dev": "taskr", + "types": "tsc --emitDeclarationOnly", + "prettier-fix": "prettier --write ." + }, + "sideEffects": false, + "type": "module", + "exports": { + ".": "./dist/index.js", + "./*": [ + "./dist/*.js", + "./dist/*/index.js" + ], + "./next.config": "./dist/next.config.cjs" + }, + "typesVersions": { + "*": { + "*": [ + "src/*", + "src/*/index" + ], + "next.config": [ + "dist/next.config.d.cts" + ] + } + }, + "files": [ + "dist" + ], + "publishConfig": { + "typesVersions": { + "*": { + "*": [ + "dist/*.d.ts", + "dist/*/index.d.ts" + ], + "next.config": [ + "dist/next.config.d.cts" + ] + } + } + }, + "dependencies": { + "@vercel/commerce": "^0.0.1", + "@vercel/fetch": "^6.1.1", + "deserialize-json-api": "^1.4.0", + "js-cookies": "^1.0.4", + "json-api-response-converter": "^1.6.0", + "kitsu": "^10.0.0-alpha.22", + "urijs": "^1.19.10" + }, + "peerDependencies": { + "next": "^12", + "react": "^17", + "react-dom": "^17" + }, + "devDependencies": { + "@taskr/clear": "^1.1.0", + "@taskr/esnext": "^1.1.0", + "@taskr/watch": "^1.1.0", + "@types/node": "^17.0.8", + "@types/react": "^17.0.38", + "lint-staged": "^12.1.7", + "next": "^12.0.8", + "prettier": "^2.5.1", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "taskr": "^1.1.0", + "taskr-swc": "^0.0.1", + "typescript": "^4.5.4" + }, + "lint-staged": { + "**/*.{js,jsx,ts,tsx,json}": [ + "prettier --write", + "git add" + ] + } +} diff --git a/packages/appibase/src/api/endpoints/cart/index.ts b/packages/appibase/src/api/endpoints/cart/index.ts new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/packages/appibase/src/api/endpoints/cart/index.ts @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/packages/appibase/src/api/endpoints/catalog/index.ts b/packages/appibase/src/api/endpoints/catalog/index.ts new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/packages/appibase/src/api/endpoints/catalog/index.ts @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/packages/appibase/src/api/endpoints/catalog/products.ts b/packages/appibase/src/api/endpoints/catalog/products.ts new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/packages/appibase/src/api/endpoints/catalog/products.ts @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/packages/appibase/src/api/endpoints/checkout/index.ts b/packages/appibase/src/api/endpoints/checkout/index.ts new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/packages/appibase/src/api/endpoints/checkout/index.ts @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/packages/appibase/src/api/endpoints/customer/address.ts b/packages/appibase/src/api/endpoints/customer/address.ts new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/packages/appibase/src/api/endpoints/customer/address.ts @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/packages/appibase/src/api/endpoints/customer/card.ts b/packages/appibase/src/api/endpoints/customer/card.ts new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/packages/appibase/src/api/endpoints/customer/card.ts @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/packages/appibase/src/api/endpoints/customer/index.ts b/packages/appibase/src/api/endpoints/customer/index.ts new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/packages/appibase/src/api/endpoints/customer/index.ts @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/packages/appibase/src/api/endpoints/login/index.ts b/packages/appibase/src/api/endpoints/login/index.ts new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/packages/appibase/src/api/endpoints/login/index.ts @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/packages/appibase/src/api/endpoints/logout/index.ts b/packages/appibase/src/api/endpoints/logout/index.ts new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/packages/appibase/src/api/endpoints/logout/index.ts @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/packages/appibase/src/api/endpoints/signup/index.ts b/packages/appibase/src/api/endpoints/signup/index.ts new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/packages/appibase/src/api/endpoints/signup/index.ts @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/packages/appibase/src/api/endpoints/wishlist/index.tsx b/packages/appibase/src/api/endpoints/wishlist/index.tsx new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/packages/appibase/src/api/endpoints/wishlist/index.tsx @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/packages/appibase/src/api/index.ts b/packages/appibase/src/api/index.ts new file mode 100644 index 000000000..84862128e --- /dev/null +++ b/packages/appibase/src/api/index.ts @@ -0,0 +1,43 @@ +import type { CommerceAPI, CommerceAPIConfig } from '@vercel/commerce/api' +import { getCommerceApi as commerceApi } from '@vercel/commerce/api' +import createFetcher from './utils/fetch-appibase' +import { API_URL } from '../const' + +import getAllPages from './operations/get-all-pages' +import getPage from './operations/get-page' +import getSiteInfo from './operations/get-site-info' +import getCustomerWishlist from './operations/get-customer-wishlist' +import getAllProductPaths from './operations/get-all-product-paths' +import getAllProducts from './operations/get-all-products' +import getProduct from './operations/get-product' + +export interface LocalConfig extends CommerceAPIConfig {} +const config: LocalConfig = { + commerceUrl: API_URL, + apiToken: '', + cartCookie: '', + customerCookie: '', + cartCookieMaxAge: 2592000, + fetch: createFetcher(() => getCommerceApi().getConfig()), +} + +const operations = { + getAllPages, + getPage, + getSiteInfo, + getCustomerWishlist, + getAllProductPaths, + getAllProducts, + getProduct, +} + +export const provider = { config, operations } + +export type Provider = typeof provider +export type LocalAPI

= CommerceAPI

+ +export function getCommerceApi

( + customProvider: P = provider as any +): LocalAPI

{ + return commerceApi(customProvider as any) +} diff --git a/packages/appibase/src/api/operations/get-all-pages.ts b/packages/appibase/src/api/operations/get-all-pages.ts new file mode 100644 index 000000000..098746040 --- /dev/null +++ b/packages/appibase/src/api/operations/get-all-pages.ts @@ -0,0 +1,21 @@ +export type Page = { url: string } +export type GetAllPagesResult = { pages: Page[] } +import type { LocalConfig } from '../index' + +export default function getAllPagesOperation() { + function getAllPages({ + config, + preview, + }: { + url?: string + config?: Partial + preview?: boolean + }): Promise { + console.log('GETTING ALL PAGES'); + + return Promise.resolve({ + pages: [], + }) + } + return getAllPages +} diff --git a/packages/appibase/src/api/operations/get-all-product-paths.ts b/packages/appibase/src/api/operations/get-all-product-paths.ts new file mode 100644 index 000000000..705a77cda --- /dev/null +++ b/packages/appibase/src/api/operations/get-all-product-paths.ts @@ -0,0 +1,17 @@ +import data from '../../data.json' + +export type GetAllProductPathsResult = { + products: Array<{ path: string }> +} + +export default function getAllProductPathsOperation() { + function getAllProductPaths(): Promise { + console.log('GETTING ALL PATHS'); + + return Promise.resolve({ + products: [] //data.products.map(({ path }) => ({ path })), + }) + } + + return getAllProductPaths +} diff --git a/packages/appibase/src/api/operations/get-all-products.ts b/packages/appibase/src/api/operations/get-all-products.ts new file mode 100644 index 000000000..ce15df8eb --- /dev/null +++ b/packages/appibase/src/api/operations/get-all-products.ts @@ -0,0 +1,33 @@ +import { Product } from '@vercel/commerce/types/product' +import { GetAllProductsOperation } from '@vercel/commerce/types/product' +import type { OperationContext } from '@vercel/commerce/api/operations' +import type { LocalConfig, Provider } from '../index' +import { NormalizeProduct } from '../utils/normalize' +import type { AppibaseProduct } from '../../types' + +export default function getAllProductsOperation({ + commerce, +}: OperationContext) { + async function getAllProducts({ + query = '', + variables, + config, + }: { + query?: string + variables?: T['variables'] + config?: Partial + preview?: boolean + } = {}): Promise<{ products: Product[] | any[] }> { + + const { fetch } = commerce.getConfig(config) + + const { data: fetchedProducts } = await fetch('/products?filter[is_parent_true]=true&include=prices,options'); + + const products = fetchedProducts.map((p : AppibaseProduct) => NormalizeProduct(p)) + + return { + products + } + } + return getAllProducts +} diff --git a/packages/appibase/src/api/operations/get-customer-wishlist.ts b/packages/appibase/src/api/operations/get-customer-wishlist.ts new file mode 100644 index 000000000..8c34b9e87 --- /dev/null +++ b/packages/appibase/src/api/operations/get-customer-wishlist.ts @@ -0,0 +1,6 @@ +export default function getCustomerWishlistOperation() { + function getCustomerWishlist(): any { + return { wishlist: {} } + } + return getCustomerWishlist +} diff --git a/packages/appibase/src/api/operations/get-page.ts b/packages/appibase/src/api/operations/get-page.ts new file mode 100644 index 000000000..bb4f6ecdc --- /dev/null +++ b/packages/appibase/src/api/operations/get-page.ts @@ -0,0 +1,15 @@ +export type Page = any +export type GetPageResult = { page?: Page } + +export type PageVariables = { + id: number +} + +export default function getPageOperation() { + function getPage(): Promise { + console.log('GETTING PAGE'); + + return Promise.resolve({}) + } + return getPage +} diff --git a/packages/appibase/src/api/operations/get-product.ts b/packages/appibase/src/api/operations/get-product.ts new file mode 100644 index 000000000..bb8a27637 --- /dev/null +++ b/packages/appibase/src/api/operations/get-product.ts @@ -0,0 +1,29 @@ +import type { LocalConfig } from '../index' +import { Product } from '@vercel/commerce/types/product' +import { GetProductOperation } from '@vercel/commerce/types/product' +import type { OperationContext } from '@vercel/commerce/api/operations' +import { NormalizeProduct } from '../utils/normalize' + +export default function getProductOperation({ + commerce, +}: OperationContext) { + async function getProduct({ + query = '', + variables, + config, + }: { + query?: string + variables?: T['variables'] + config?: Partial + preview?: boolean + } = {}): Promise { + const { fetch } = commerce.getConfig(config) + const { data: fetchedProduct } = await fetch(`/products/${variables?.slug}?include=products,variations,options,prices,stock_items,variations.variation_options`); + + return { + product: NormalizeProduct(fetchedProduct), + } + } + + return getProduct +} diff --git a/packages/appibase/src/api/operations/get-site-info.ts b/packages/appibase/src/api/operations/get-site-info.ts new file mode 100644 index 000000000..02066a990 --- /dev/null +++ b/packages/appibase/src/api/operations/get-site-info.ts @@ -0,0 +1,38 @@ +import { OperationContext } from '@vercel/commerce/api/operations' +import { Category } from '@vercel/commerce/types/site' +import { NormalizeCategory } from '../utils/normalize' +import { LocalConfig } from '../index' +import { AppibaseCollection } from '../../types' + +export type GetSiteInfoResult< + T extends { categories: any[]; brands: any[] } = { + categories: Category[] + brands: any[] + } +> = T + +export default function getSiteInfoOperation({ commerce }: OperationContext) { + async function getSiteInfo({ + query, + variables, + config, + }: { + query?: string + variables?: any + config?: Partial + preview?: boolean + } = {}): Promise { + + const { fetch } = commerce.getConfig(config) + + const { res : { data } } = await fetch('/collections'); + const categories = data.map((p : AppibaseCollection) => NormalizeCategory(p)) + + return Promise.resolve({ + categories, + brands: [], + }) + } + + return getSiteInfo +} diff --git a/packages/appibase/src/api/operations/index.ts b/packages/appibase/src/api/operations/index.ts new file mode 100644 index 000000000..086fdf83a --- /dev/null +++ b/packages/appibase/src/api/operations/index.ts @@ -0,0 +1,6 @@ +export { default as getPage } from './get-page' +export { default as getSiteInfo } from './get-site-info' +export { default as getAllPages } from './get-all-pages' +export { default as getProduct } from './get-product' +export { default as getAllProducts } from './get-all-products' +export { default as getAllProductPaths } from './get-all-product-paths' diff --git a/packages/appibase/src/api/utils/access-token.ts b/packages/appibase/src/api/utils/access-token.ts new file mode 100644 index 000000000..e07194b28 --- /dev/null +++ b/packages/appibase/src/api/utils/access-token.ts @@ -0,0 +1,35 @@ +import Cookies from 'js-cookie' +import Kitsu from "kitsu"; +import { API_URL, SCOPE, CLIENT_ID_STOREFRONT } from '../../const' + +const GetAccessToken = async () => { + + const fromCookies = Cookies.get('access_token'); + + if(!fromCookies) { + const myHeaders = new Headers(); + myHeaders.append("Content-Type", "application/x-www-form-urlencoded"); + myHeaders.append("Authorization", `Basic ${Buffer.from(CLIENT_ID_STOREFRONT).toString('base64')}`); + myHeaders.append("Accept", "application/json"); + + const urlencoded = new URLSearchParams(); + urlencoded.append("grant_type", "client_credentials"); + urlencoded.append("scope", SCOPE); + + const requestOptions = { + method: 'POST', + headers: myHeaders, + body: urlencoded, + }; + + const response = await fetch(API_URL + "/oauth/token", requestOptions) + const { access_token, expires_in } = await response.json() + + Cookies.set('access_token', access_token, { expires: expires_in / 60 / 60 / 24 }) + return access_token + } + else return fromCookies +} + + +export { GetAccessToken } \ No newline at end of file diff --git a/packages/appibase/src/api/utils/fetch-appibase.ts b/packages/appibase/src/api/utils/fetch-appibase.ts new file mode 100644 index 000000000..9af60fe1a --- /dev/null +++ b/packages/appibase/src/api/utils/fetch-appibase.ts @@ -0,0 +1,28 @@ +import { FetcherError } from '@vercel/commerce/utils/errors' +import type { GraphQLFetcher } from '@vercel/commerce/api' +import Kitsu from "kitsu"; +import type { LocalConfig } from '../index' +import { API_URL } from '../../const' +import { GetAccessToken } from './access-token' + +const fetchApi: (getConfig: () => LocalConfig) => GraphQLFetcher = + (getConfig) => + async (query: string, { variables, preview } = {}, fetchOptions) => { + const config = getConfig() + + const api = new Kitsu({ baseURL: API_URL + '/api/v1' }) + api.headers.Authorization = `Bearer ${await GetAccessToken()}` + + const res = await api.get(query) + + if (res.errors) { + throw new FetcherError({ + errors: res.errors ?? [{ message: 'Failed to fetch for API' }], + status: res.status, + }) + } + + return { data: res.data, res } + } + +export default fetchApi diff --git a/packages/appibase/src/api/utils/fetch.ts b/packages/appibase/src/api/utils/fetch.ts new file mode 100644 index 000000000..9d9fff3ed --- /dev/null +++ b/packages/appibase/src/api/utils/fetch.ts @@ -0,0 +1,3 @@ +import zeitFetch from '@vercel/fetch' + +export default zeitFetch() diff --git a/packages/appibase/src/api/utils/normalize.ts b/packages/appibase/src/api/utils/normalize.ts new file mode 100644 index 000000000..a5bc725d0 --- /dev/null +++ b/packages/appibase/src/api/utils/normalize.ts @@ -0,0 +1,82 @@ +import { Product, ProductOption, ProductImage } from '@vercel/commerce/types/product' +import { Category } from '@vercel/commerce/types/site' +import { Cart, LineItem } from '@vercel/commerce/types/cart' +import type { AppibaseProduct, AppibaseCollection, AppibaseCart } from '../../types' + + +const NormalizeProduct = (product: AppibaseProduct): Product => { + const options: ProductOption[] = []; + + for(const variation of (product.variations?.data || [])) { + const option : ProductOption | undefined = options.find(o => o.displayName === variation.name); + if(!option) { + options.push({ + id: `option-${variation.name.toLowerCase()}`, + displayName : variation.name, values: variation.options?.map(o => ({ label: o.name })) || [] + }); + } + } + + return { + id: product.id, + name: product.name, + description: product.description, + images: product.image_urls.map(i => { url: i }), + sku: product.sku, + slug: product.sku, + variants: product.children?.data?.map(p => ({ + id: p.id, + options: p.variation_options?.data.map(o => ({ + __typename: "MultipleChoiceOption", + id: o.id, + displayName: o.variation_name || "", + values: [{ label: o.name || "" }] + })) || [] + })) || [], + price: { value: product.prices.data[0].amount.float, currencyCode: product.prices.data[0].currency }, + options + } +} + +const NormalizeCategory = (collection: AppibaseCollection): Category => { + return { + id: String(collection.id), + name: collection.name, + slug: collection.slug, + path: '/' + collection.slug, + } +} + +const NormalizeCart = (cart: AppibaseCart): Cart => { + return { + id: String(cart.id), + createdAt: (new Date()).toDateString(), + currency: { + code: cart.currency + }, + taxesIncluded: cart.tax_incl, + lineItemsSubtotalPrice: cart.subtotal_amount.float, + subtotalPrice: cart.subtotal_amount.float, + totalPrice: cart.total_amount.float, + lineItems: cart.cart_items?.data.map(i => ({ + id: i.id, + variantId: i.id, + productId: i.id, + name: i.name, + discounts: [], + path: i.sku, + variant: { + sku: i.sku, + id: i.id, + name: i.name, + requiresShipping: true, + price: i.price.float, + listPrice: i.price.float, + image: { url: i.image_url } + }, + quantity: parseInt(i.quantity) + })) || [] + } +} + +export { NormalizeProduct, NormalizeCategory, NormalizeCart } \ No newline at end of file diff --git a/packages/appibase/src/auth/index.ts b/packages/appibase/src/auth/index.ts new file mode 100644 index 000000000..36e757a89 --- /dev/null +++ b/packages/appibase/src/auth/index.ts @@ -0,0 +1,3 @@ +export { default as useLogin } from './use-login' +export { default as useLogout } from './use-logout' +export { default as useSignup } from './use-signup' diff --git a/packages/appibase/src/auth/use-login.tsx b/packages/appibase/src/auth/use-login.tsx new file mode 100644 index 000000000..20e3ed229 --- /dev/null +++ b/packages/appibase/src/auth/use-login.tsx @@ -0,0 +1,16 @@ +import { MutationHook } from '@vercel/commerce/utils/types' +import useLogin, { UseLogin } from '@vercel/commerce/auth/use-login' + +export default useLogin as UseLogin + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher() { + return null + }, + useHook: () => () => { + return async function () {} + }, +} diff --git a/packages/appibase/src/auth/use-logout.tsx b/packages/appibase/src/auth/use-logout.tsx new file mode 100644 index 000000000..4e74908f3 --- /dev/null +++ b/packages/appibase/src/auth/use-logout.tsx @@ -0,0 +1,17 @@ +import { MutationHook } from '@vercel/commerce/utils/types' +import useLogout, { UseLogout } from '@vercel/commerce/auth/use-logout' + +export default useLogout as UseLogout + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher() { + return null + }, + useHook: + ({ fetch }) => + () => + async () => {}, +} diff --git a/packages/appibase/src/auth/use-signup.tsx b/packages/appibase/src/auth/use-signup.tsx new file mode 100644 index 000000000..e48811403 --- /dev/null +++ b/packages/appibase/src/auth/use-signup.tsx @@ -0,0 +1,19 @@ +import { useCallback } from 'react' +import useCustomer from '../customer/use-customer' +import { MutationHook } from '@vercel/commerce/utils/types' +import useSignup, { UseSignup } from '@vercel/commerce/auth/use-signup' + +export default useSignup as UseSignup + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher() { + return null + }, + useHook: + ({ fetch }) => + () => + () => {}, +} diff --git a/packages/appibase/src/cart/index.ts b/packages/appibase/src/cart/index.ts new file mode 100644 index 000000000..3b8ba990e --- /dev/null +++ b/packages/appibase/src/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 useRemoveItem } from './use-remove-item' +export { default as useUpdateItem } from './use-update-item' diff --git a/packages/appibase/src/cart/use-add-item.tsx b/packages/appibase/src/cart/use-add-item.tsx new file mode 100644 index 000000000..0eb469050 --- /dev/null +++ b/packages/appibase/src/cart/use-add-item.tsx @@ -0,0 +1,39 @@ +import { useCallback } from 'react'; +import useAddItem, { UseAddItem } from '@vercel/commerce/cart/use-add-item' +import { MutationHook } from '@vercel/commerce/utils/types' +import Cookies from 'js-cookie' +import { NormalizeCart } from '../api/utils/normalize' + +export default useAddItem as UseAddItem +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher({ input: item, options, fetch }) { + + const fromCookies = Cookies.get('cart_id'); + + await fetch({ + query : `/carts/${fromCookies}/cart_items`, + method: 'POST', + body: { + quantity: 1, + item: { + data : { + type: 'product', + id: item.variantId + } + } + } + }) + return; + }, + useHook: + ({ fetch }) => + () => { + return useCallback(async function addItem(input) { + const data = await fetch({ input }); + return data; + }, [fetch]); + }, +} diff --git a/packages/appibase/src/cart/use-cart.tsx b/packages/appibase/src/cart/use-cart.tsx new file mode 100644 index 000000000..965a8eed3 --- /dev/null +++ b/packages/appibase/src/cart/use-cart.tsx @@ -0,0 +1,51 @@ +import { useMemo } from 'react' +import { SWRHook } from '@vercel/commerce/utils/types' +import useCart, { UseCart } from '@vercel/commerce/cart/use-cart' +import Cookies from 'js-cookie' +import { NormalizeCart } from '../api/utils/normalize' +import { STORE_ID } from '../const' + +export default useCart as UseCart + +export const handler: SWRHook = { + fetchOptions: { + query: '' + }, + async fetcher({ fetch }) { + const fromCookies = Cookies.get('cart_id'); + + if(!fromCookies) { + + const { data } = await fetch({ query : `/stores/${STORE_ID}/carts`, method: "POST", body: { id: '' } }) + + Cookies.set('cart_id', data.id) + + return NormalizeCart(data) + } + + else { + const { data } = await fetch({ query : `/carts/${fromCookies}?include=cart_items` }) + return NormalizeCart(data) + } + }, + useHook: + ({ useData }) => + (input) => { + const response = useData({ + swrOptions: { revalidateOnFocus: true, ...input?.swrOptions }, + }) + + return useMemo( + () => + Object.create(response, { + isEmpty: { + get() { + return (response.data?.lineItems.length ?? 0) <= 0 + }, + enumerable: true, + }, + }), + [response] + ) + }, +} diff --git a/packages/appibase/src/cart/use-remove-item.tsx b/packages/appibase/src/cart/use-remove-item.tsx new file mode 100644 index 000000000..0da9169b2 --- /dev/null +++ b/packages/appibase/src/cart/use-remove-item.tsx @@ -0,0 +1,40 @@ +import { MutationHook } from '@vercel/commerce/utils/types' +import { useCallback } from 'react' +import useRemoveItem, { + UseRemoveItem, +} from '@vercel/commerce/cart/use-remove-item' +import Cookies from 'js-cookie' +import { NormalizeCart } from '../api/utils/normalize' +import useCart from './use-cart' + +export default useRemoveItem as UseRemoveItem + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher({ input: item, options, fetch }) { + await fetch({ + query : `cart_items`, + method: 'DELETE', + body: item.id + }) + + const fromCookies = Cookies.get('cart_id'); + const { data } = await fetch({ query : `/carts/${fromCookies}?include=cart_items` }) + return NormalizeCart(data) + }, + useHook: + ({ fetch }) => + () => { + const { mutate } = useCart() + + return useCallback(async function removeItem(input) { + const data = await fetch({ input }); + + await mutate(data, true) + + return data; + }, [fetch, mutate]); + }, +} diff --git a/packages/appibase/src/cart/use-update-item.tsx b/packages/appibase/src/cart/use-update-item.tsx new file mode 100644 index 000000000..c45f8c601 --- /dev/null +++ b/packages/appibase/src/cart/use-update-item.tsx @@ -0,0 +1,45 @@ +import { MutationHook } from '@vercel/commerce/utils/types' +import { useCallback } from 'react' +import useUpdateItem, { + UseUpdateItem, +} from '@vercel/commerce/cart/use-update-item' +import { NormalizeCart } from '../api/utils/normalize' +import Cookies from 'js-cookie' +import useCart from './use-cart' + +export default useUpdateItem as UseUpdateItem + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher({ input, options, fetch }) { + await fetch({ + query : `cart_items`, + method: 'PATCH', + body: { + id: input.item.id, + type: 'cart_item', + quantity: String(input.quantity) + } + }) + + const fromCookies = Cookies.get('cart_id'); + const { data } = await fetch({ query : `/carts/${fromCookies}?include=cart_items` }) + return NormalizeCart(data) + + }, + useHook: + ({ fetch }) => + ({ item }) => { + const { mutate } = useCart() + + return useCallback(async function updateItem(input) { + const data = await fetch({ input: { item, quantity: input.quantity } }); + + await mutate(data, true) + + return data; + }, [fetch, mutate]); + }, +} diff --git a/packages/appibase/src/checkout/use-checkout.tsx b/packages/appibase/src/checkout/use-checkout.tsx new file mode 100644 index 000000000..76997be73 --- /dev/null +++ b/packages/appibase/src/checkout/use-checkout.tsx @@ -0,0 +1,16 @@ +import { SWRHook } from '@vercel/commerce/utils/types' +import useCheckout, { + UseCheckout, +} from '@vercel/commerce/checkout/use-checkout' + +export default useCheckout as UseCheckout + +export const handler: SWRHook = { + fetchOptions: { + query: '', + }, + async fetcher({ input, options, fetch }) {}, + useHook: + ({ useData }) => + async (input) => ({}), +} diff --git a/packages/appibase/src/commerce.config.json b/packages/appibase/src/commerce.config.json new file mode 100644 index 000000000..f46c1d6b3 --- /dev/null +++ b/packages/appibase/src/commerce.config.json @@ -0,0 +1,10 @@ +{ + "provider": "appibase", + "features": { + "wishlist": false, + "cart": true, + "search": true, + "customerAuth": false, + "customCheckout": true + } +} diff --git a/packages/appibase/src/const.ts b/packages/appibase/src/const.ts new file mode 100644 index 000000000..b71e7d61e --- /dev/null +++ b/packages/appibase/src/const.ts @@ -0,0 +1,7 @@ +export const API_URL = `https://appibase.com` + +export const SCOPE = `storefront client` + +export const CLIENT_ID_STOREFRONT = process.env.APPIBASE_CLIENT_ID_STOREFRONT + +export const STORE_ID = process.env.APPIBASE_STORE_ID diff --git a/packages/appibase/src/customer/address/use-add-item.tsx b/packages/appibase/src/customer/address/use-add-item.tsx new file mode 100644 index 000000000..4f85c8472 --- /dev/null +++ b/packages/appibase/src/customer/address/use-add-item.tsx @@ -0,0 +1,17 @@ +import useAddItem, { + UseAddItem, +} from '@vercel/commerce/customer/address/use-add-item' +import { MutationHook } from '@vercel/commerce/utils/types' + +export default useAddItem as UseAddItem + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher({ input, options, fetch }) {}, + useHook: + ({ fetch }) => + () => + async () => ({}), +} diff --git a/packages/appibase/src/customer/card/use-add-item.tsx b/packages/appibase/src/customer/card/use-add-item.tsx new file mode 100644 index 000000000..77d149eff --- /dev/null +++ b/packages/appibase/src/customer/card/use-add-item.tsx @@ -0,0 +1,17 @@ +import useAddItem, { + UseAddItem, +} from '@vercel/commerce/customer/card/use-add-item' +import { MutationHook } from '@vercel/commerce/utils/types' + +export default useAddItem as UseAddItem + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher({ input, options, fetch }) {}, + useHook: + ({ fetch }) => + () => + async () => ({}), +} diff --git a/packages/appibase/src/customer/index.ts b/packages/appibase/src/customer/index.ts new file mode 100644 index 000000000..6c903ecc5 --- /dev/null +++ b/packages/appibase/src/customer/index.ts @@ -0,0 +1 @@ +export { default as useCustomer } from './use-customer' diff --git a/packages/appibase/src/customer/use-customer.tsx b/packages/appibase/src/customer/use-customer.tsx new file mode 100644 index 000000000..04c48943d --- /dev/null +++ b/packages/appibase/src/customer/use-customer.tsx @@ -0,0 +1,17 @@ +import { SWRHook } from '@vercel/commerce/utils/types' +import useCustomer, { + UseCustomer, +} from '@vercel/commerce/customer/use-customer' + +export default useCustomer as UseCustomer +export const handler: SWRHook = { + fetchOptions: { + query: '', + }, + async fetcher({ input, options, fetch }) {}, + useHook: () => () => { + return async function addItem() { + return {} + } + }, +} diff --git a/packages/appibase/src/data.json b/packages/appibase/src/data.json new file mode 100644 index 000000000..18c8ee718 --- /dev/null +++ b/packages/appibase/src/data.json @@ -0,0 +1,235 @@ +{ + "products": [ + { + "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjA=", + "name": "New Short Sleeve T-Shirt", + "vendor": "Next.js", + "path": "/new-short-sleeve-t-shirt", + "slug": "new-short-sleeve-t-shirt", + "price": { "value": 25, "currencyCode": "USD" }, + "descriptionHtml": "

Show off your love for Next.js and Vercel with this unique, limited edition t-shirt. This design is part of a limited run, numbered drop at the June 2021 Next.js Conf. It features a unique, handcrafted triangle design. Get it while supplies last – only 200 of these shirts will be made! All proceeds will be donated to charity.

", + "images": [ + { + "url": "/assets/drop-shirt-0.png", + "altText": "Shirt", + "width": 1000, + "height": 1000 + }, + { + "url": "/assets/drop-shirt-1.png", + "altText": "Shirt", + "width": 1000, + "height": 1000 + }, + { + "url": "/assets/drop-shirt-2.png", + "altText": "Shirt", + "width": 1000, + "height": 1000 + } + ], + "variants": [ + { + "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjAss=", + "options": [ + { + "__typename": "MultipleChoiceOption", + "id": "asd", + "displayName": "Size", + "values": [ + { + "label": "XL" + } + ] + } + ] + } + ], + "options": [ + { + "id": "option-color", + "displayName": "Color", + "values": [ + { + "label": "color", + "hexColors": ["#222"] + } + ] + }, + { + "id": "option-size", + "displayName": "Size", + "values": [ + { + "label": "S" + }, + { + "label": "M" + }, + { + "label": "L" + } + ] + } + ] + }, + { + "id": "Z2lkOi8vc2hvcGlmeS9Qcm9ksdWN0LzU0NDczMjUwMjQ0MjA=", + "name": "Lightweight Jacket", + "vendor": "Next.js", + "path": "/lightweight-jacket", + "slug": "lightweight-jacket", + "price": { "value": 249.99, "currencyCode": "USD" }, + "descriptionHtml": "

Show off your love for Next.js and Vercel with this unique, limited edition t-shirt. This design is part of a limited run, numbered drop at the June 2021 Next.js Conf. It features a unique, handcrafted triangle design. Get it while supplies last – only 200 of these shirts will be made! All proceeds will be donated to charity.

", + "images": [ + { + "url": "/assets/lightweight-jacket-0.png", + "altText": "Lightweight Jacket", + "width": 1000, + "height": 1000 + }, + { + "url": "/assets/lightweight-jacket-1.png", + "altText": "Lightweight Jacket", + "width": 1000, + "height": 1000 + }, + { + "url": "/assets/lightweight-jacket-2.png", + "altText": "Lightweight Jacket", + "width": 1000, + "height": 1000 + } + ], + "variants": [ + { + "id": "Z2lkOid8vc2hvcGlmeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjAss=", + "options": [ + { + "__typename": "MultipleChoiceOption", + "id": "asd", + "displayName": "Size", + "values": [ + { + "label": "XL" + } + ] + } + ] + } + ], + "options": [ + { + "id": "option-color", + "displayName": "Color", + "values": [ + { + "label": "color", + "hexColors": ["#222"] + } + ] + }, + { + "id": "option-size", + "displayName": "Size", + "values": [ + { + "label": "S" + }, + { + "label": "M" + }, + { + "label": "L" + } + ] + } + ] + }, + { + "id": "Z2lkOis8vc2hvcGlmsddeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjA=", + "name": "Shirt", + "vendor": "Next.js", + "path": "/shirt", + "slug": "shirt", + "price": { "value": 25, "currencyCode": "USD" }, + "descriptionHtml": "

Show off your love for Next.js and Vercel with this unique, limited edition t-shirt. This design is part of a limited run, numbered drop at the June 2021 Next.js Conf. It features a unique, handcrafted triangle design. Get it while supplies last – only 200 of these shirts will be made! All proceeds will be donated to charity.

", + "images": [ + { + "url": "/assets/t-shirt-0.png", + "altText": "Shirt", + "width": 1000, + "height": 1000 + }, + { + "url": "/assets/t-shirt-1.png", + "altText": "Shirt", + "width": 1000, + "height": 1000 + }, + { + "url": "/assets/t-shirt-2.png", + "altText": "Shirt", + "width": 1000, + "height": 1000 + }, + { + "url": "/assets/t-shirt-3.png", + "altText": "Shirt", + "width": 1000, + "height": 1000 + }, + { + "url": "/assets/t-shirt-4.png", + "altText": "Shirt", + "width": 1000, + "height": 1000 + } + ], + "variants": [ + { + "id": "Z2lkOi8vc2hvcGlmeS9Qcms9kdWN0LzU0NDczMjUwMjQ0MjAss=", + "options": [ + { + "__typename": "MultipleChoiceOption", + "id": "asd", + "displayName": "Size", + "values": [ + { + "label": "XL" + } + ] + } + ] + } + ], + "options": [ + { + "id": "option-color", + "displayName": "Color", + "values": [ + { + "label": "color", + "hexColors": ["#222"] + } + ] + }, + { + "id": "option-size", + "displayName": "Size", + "values": [ + { + "label": "S" + }, + { + "label": "M" + }, + { + "label": "L" + } + ] + } + ] + } + ] +} diff --git a/packages/appibase/src/fetcher.ts b/packages/appibase/src/fetcher.ts new file mode 100644 index 000000000..f9d5eb1e5 --- /dev/null +++ b/packages/appibase/src/fetcher.ts @@ -0,0 +1,25 @@ +import { Fetcher } from '@vercel/commerce/utils/types' +import { API_URL } from './const' +import { handleFetchResponse } from './utils' +import { GetAccessToken } from './api/utils/access-token' +import Kitsu from "kitsu"; + +const fetcher: Fetcher = async ({ + method = 'GET', + variables, + query = '', + body +}) => { + const { locale, ...vars } = variables ?? {} + + const api = new Kitsu({ baseURL: API_URL + '/api/v1', camelCaseTypes: false, pluralize: false }) + api.headers.Authorization = `Bearer ${await GetAccessToken()}` + + if(method === 'GET') return await api.get(query) + else if(method === 'POST') return await api.create(query, body) + else if(method === 'DELETE') return await api.remove(query, body) + else if(method === 'PATCH') return await api.update(query, body) +} + +export default fetcher + diff --git a/packages/appibase/src/index.tsx b/packages/appibase/src/index.tsx new file mode 100644 index 000000000..6bdaba8ba --- /dev/null +++ b/packages/appibase/src/index.tsx @@ -0,0 +1,12 @@ +import { + getCommerceProvider, + useCommerce as useCoreCommerce, +} from '@vercel/commerce' +import { appibaseProvider, AppibaseProvider } from './provider' + +export { appibaseProvider } +export type { AppibaseProvider } + +export const CommerceProvider = getCommerceProvider(appibaseProvider) + +export const useCommerce = () => useCoreCommerce() diff --git a/packages/appibase/src/next.config.cjs b/packages/appibase/src/next.config.cjs new file mode 100644 index 000000000..9885a2bae --- /dev/null +++ b/packages/appibase/src/next.config.cjs @@ -0,0 +1,8 @@ +const commerce = require('./commerce.config.json') + +module.exports = { + commerce, + images: { + domains: ['localhost', 'shopify.vercel.store', 'vercel.store', 'demo.vercel.store'], + }, +} diff --git a/packages/appibase/src/product/index.ts b/packages/appibase/src/product/index.ts new file mode 100644 index 000000000..426a3edcd --- /dev/null +++ b/packages/appibase/src/product/index.ts @@ -0,0 +1,2 @@ +export { default as usePrice } from './use-price' +export { default as useSearch } from './use-search' diff --git a/packages/appibase/src/product/use-price.tsx b/packages/appibase/src/product/use-price.tsx new file mode 100644 index 000000000..fd42d7033 --- /dev/null +++ b/packages/appibase/src/product/use-price.tsx @@ -0,0 +1,2 @@ +export * from '@vercel/commerce/product/use-price' +export { default } from '@vercel/commerce/product/use-price' diff --git a/packages/appibase/src/product/use-search.tsx b/packages/appibase/src/product/use-search.tsx new file mode 100644 index 000000000..8dc929c7c --- /dev/null +++ b/packages/appibase/src/product/use-search.tsx @@ -0,0 +1,54 @@ +import { SWRHook } from '@vercel/commerce/utils/types' +import useSearch, { UseSearch } from '@vercel/commerce/product/use-search' +export default useSearch as UseSearch +import type { AppibaseProduct } from '../types' +import { NormalizeProduct } from '../api/utils/normalize' + +export type SearchProductsInput = { + search?: string + categoryId?: number | string + brandId?: number + sort?: string + locale?: string +} + +export const handler: SWRHook = { + fetchOptions: { + url: '/products?filter[is_parent_true]=true&include=prices', + method: 'GET', + }, + async fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) { + let url = options.url + + if (search) url += `&filter[name_cont_all]=${search}` + if (Number.isInteger(Number(categoryId))) + url += `&filter[collections_id_eq]=${categoryId}` + // if (Number.isInteger(brandId)) + // url.searchParams.set('brandId', String(brandId)) + // if (sort) url.searchParams.set('sort', sort) + + + const response = await fetch({ + query: url, + method: options.method, + }) + + return { found: response.data.length, products: response.data.map((p : AppibaseProduct) => NormalizeProduct(p) ) } + }, + 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/packages/appibase/src/provider.ts b/packages/appibase/src/provider.ts new file mode 100644 index 000000000..818967643 --- /dev/null +++ b/packages/appibase/src/provider.ts @@ -0,0 +1,22 @@ +import fetcher from './fetcher' +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 appibaseProvider = { + locale: 'en-us', + cartCookie: 'session', + fetcher: fetcher, + cart: { useCart, useAddItem, useUpdateItem, useRemoveItem }, + customer: { useCustomer }, + products: { useSearch }, + auth: { useLogin, useLogout, useSignup }, +} + +export type AppibaseProvider = typeof appibaseProvider diff --git a/packages/appibase/src/types.ts b/packages/appibase/src/types.ts new file mode 100644 index 000000000..e55fc90f6 --- /dev/null +++ b/packages/appibase/src/types.ts @@ -0,0 +1,132 @@ +export type AppibaseVariation = { + id: string + name: string + options?: AppibaseVariationOption[] +} + +export type AppibaseVariationOption = { + id: string + name: string + variation_name?: string + variation?: AppibaseVariation +} + +export type PriceList = { + id: number + name: string + description?: string + currency: string + tax_incl: boolean +} + +export type Amount = { + cents: number + float: number + formatted: string +} + +export type AppibasePrices = { + data: AppibasePrice[] +} + +export type AppibasePrice = { + id: number + currency: string + amount: Amount + original_amount: Amount + price_list?: PriceList + product?: AppibaseProduct +} + +export type StockLocation = { + id: number + name: string + description?: string +} + +export type StockItem = { + id: number + quantity: number + reserved: number + available: number + stock_location?: StockLocation + product?: AppibaseProduct +} + +export type AppibaseVariations = { + data: AppibaseVariation[] +} + +export type AppibaseProductChildren = { + data: AppibaseProduct[] +} + +export type AppibaseVariationOptions = { + data: AppibaseVariationOption[] +} + +export type AppibaseProduct = { + id: string + name: string + description: string + slug: string + sku: string + category: string + vendor: string + tags: string[] + image_urls: string[] + is_parent: boolean + active: boolean + livemode: boolean + parent?: AppibaseProduct + children?: AppibaseProductChildren + variations?: AppibaseVariations + variation_options?: AppibaseVariationOptions + prices: AppibasePrices + stock_items?: StockItem[] +} + +export type AppibaseCollection = { + id: number + name: string + description: string + slug: string + image_url: string + is_parent: boolean + active: boolean + livemode: boolean + parent?: AppibaseCollection + children?: AppibaseCollection[] + products?: AppibaseProduct[] +} + +export type AppibaseCartItems = { + data: AppibaseCartItem[] +} + +export type AppibaseCart = { + id: string + guest: boolean + email: string + currency: string + tax_incl: boolean + tax_rate: number + subtotal_amount: Amount + shipping_amount: Amount + tax_amount: Amount + total_amount: Amount + cart_items: AppibaseCartItems +} + +export type AppibaseCartItem = { + id: string + name: string + description: string + sku: string + image_url: string + quantity: string + currency: string + price: Amount + original_price: Amount + subtotal_amount: Amount +} \ No newline at end of file diff --git a/packages/appibase/src/utils/handle-fetch-response.ts b/packages/appibase/src/utils/handle-fetch-response.ts new file mode 100644 index 000000000..927ab54f1 --- /dev/null +++ b/packages/appibase/src/utils/handle-fetch-response.ts @@ -0,0 +1,27 @@ +import { FetcherError } from '@vercel/commerce/utils/errors' + +export function getError(errors: any[] | null, 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/packages/appibase/src/utils/index.ts b/packages/appibase/src/utils/index.ts new file mode 100644 index 000000000..5aa8f04ea --- /dev/null +++ b/packages/appibase/src/utils/index.ts @@ -0,0 +1 @@ +export { default as handleFetchResponse } from './handle-fetch-response' diff --git a/packages/appibase/src/wishlist/use-add-item.tsx b/packages/appibase/src/wishlist/use-add-item.tsx new file mode 100644 index 000000000..75f067c3a --- /dev/null +++ b/packages/appibase/src/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/packages/appibase/src/wishlist/use-remove-item.tsx b/packages/appibase/src/wishlist/use-remove-item.tsx new file mode 100644 index 000000000..a2d3a8a05 --- /dev/null +++ b/packages/appibase/src/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/packages/appibase/src/wishlist/use-wishlist.tsx b/packages/appibase/src/wishlist/use-wishlist.tsx new file mode 100644 index 000000000..b2785d46f --- /dev/null +++ b/packages/appibase/src/wishlist/use-wishlist.tsx @@ -0,0 +1,43 @@ +import { HookFetcher } from '@vercel/commerce/utils/types' +import type { Product } from '@vercel/commerce/types/product' + +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/packages/appibase/taskfile.js b/packages/appibase/taskfile.js new file mode 100644 index 000000000..39b1b2a86 --- /dev/null +++ b/packages/appibase/taskfile.js @@ -0,0 +1,20 @@ +export async function build(task, opts) { + await task + .source('src/**/*.+(ts|tsx|js)') + .swc({ dev: opts.dev, outDir: 'dist', baseUrl: 'src' }) + .target('dist') + .source('src/**/*.+(cjs|json)') + .target('dist') + task.$.log('Compiled src files') +} + +export async function release(task) { + await task.clear('dist').start('build') +} + +export default async function dev(task) { + const opts = { dev: true } + await task.clear('dist') + await task.start('build', opts) + await task.watch('src/**/*.+(ts|tsx|js|cjs|json)', 'build', opts) +} diff --git a/packages/appibase/tsconfig.json b/packages/appibase/tsconfig.json new file mode 100644 index 000000000..cd04ab2ff --- /dev/null +++ b/packages/appibase/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "outDir": "dist", + "baseUrl": "src", + "lib": ["dom", "dom.iterable", "esnext"], + "declaration": true, + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "incremental": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/site/commerce-config.js b/site/commerce-config.js index d52797e51..3c2fad564 100644 --- a/site/commerce-config.js +++ b/site/commerce-config.js @@ -19,6 +19,7 @@ const PROVIDERS = [ '@vercel/commerce-kibocommerce', '@vercel/commerce-spree', '@vercel/commerce-commercejs', + 'appibase' ] function getProviderName() { diff --git a/site/commerce.config.json b/site/commerce.config.json index ad72b58de..b9ebeb681 100644 --- a/site/commerce.config.json +++ b/site/commerce.config.json @@ -4,6 +4,6 @@ "search": true, "wishlist": false, "customerAuth": false, - "customCheckout": false + "customCheckout": true } } diff --git a/site/tsconfig.json b/site/tsconfig.json index 7c91afd6f..d4eac60b5 100644 --- a/site/tsconfig.json +++ b/site/tsconfig.json @@ -23,8 +23,8 @@ "@components/*": ["components/*"], "@commerce": ["../packages/commerce/src"], "@commerce/*": ["../packages/commerce/src/*"], - "@framework": ["../packages/local/src"], - "@framework/*": ["../packages/local/src/*"] + "@framework": ["../packages/appibase/src"], + "@framework/*": ["../packages/appibase/src/*"] } }, "include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"], diff --git a/yarn.lock b/yarn.lock index fc850a7a8..2b2092a7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1985,6 +1985,13 @@ axios@^0.21.1: dependencies: follow-redirects "^1.14.0" +axios@^0.26.0: + version "0.26.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" + integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== + dependencies: + follow-redirects "^1.14.8" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -2802,6 +2809,15 @@ dependency-graph@^0.11.0: resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27" integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg== +deserialize-json-api@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/deserialize-json-api/-/deserialize-json-api-1.4.0.tgz#776df0d56db1cab420d1d58e7b3d916f0140fda8" + integrity sha512-rfViAb34ATDUygZwuU8VSi/FC1T5m+eIAjPiTwcuwCvf3V6uWdnKSYZ1Y8myHW9RCKPhnBKMHp/3ZVF/uEZISQ== + dependencies: + lodash.camelcase "^4.3.0" + lodash.isarray "^4.0.0" + lodash.isplainobject "^4.0.6" + detect-indent@^6.0.0: version "6.1.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" @@ -3469,6 +3485,11 @@ follow-redirects@^1.14.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== +follow-redirects@^1.14.8: + version "1.14.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" + integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== + for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -4383,6 +4404,11 @@ js-cookie@^3.0.1: resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414" integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw== +js-cookies@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/js-cookies/-/js-cookies-1.0.4.tgz#d46e576c420ff6d5542c0f52b6d4ef7d637e754e" + integrity sha1-1G5XbEIP9tVULA9SttTvfWN+dU4= + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -4400,6 +4426,11 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +json-api-response-converter@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/json-api-response-converter/-/json-api-response-converter-1.6.0.tgz#66fc07c92d39da8a0a0d2eebc10b8b1b9b38d962" + integrity sha512-PetQPJUmimf5OuDPkjWij85LdNKctG9SnGQ9EfdOQxt2nwEviraXGekEgJcEwsgeKxnrG2zD7D+nQJ6YAkzM7Q== + json-buffer@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" @@ -4531,6 +4562,20 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +kitsu-core@^10.0.0-alpha.22: + version "10.0.0-alpha.22" + resolved "https://registry.yarnpkg.com/kitsu-core/-/kitsu-core-10.0.0-alpha.22.tgz#783fa6352245f147b1ddde057aef424d4d0e4da9" + integrity sha512-+1O27hmKlTxYSTOuJw0D5leJMD5KMpcRczb4k3CGKylRMyBlNqsCCFLb6FNDRIDVu4szRYKAnypljVJMgSkovg== + +kitsu@^10.0.0-alpha.22: + version "10.0.0-alpha.22" + resolved "https://registry.yarnpkg.com/kitsu/-/kitsu-10.0.0-alpha.22.tgz#d30281bad42572bed5ea78adf6fe3e7b92e93b65" + integrity sha512-EgDxOfiBEBj7n0FgEqzJSReqiXcUDwcKNKiVJcgbaCjISdkN9PdiSNTN3KWRYS4EuetLkIjWpH40VMkGAW4fjg== + dependencies: + axios "^0.26.0" + kitsu-core "^10.0.0-alpha.22" + pluralize "^8.0.0" + language-subtag-registry@~0.3.2: version "0.3.21" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a" @@ -4694,6 +4739,11 @@ lodash.includes@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= +lodash.isarray@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-4.0.0.tgz#2aca496b28c4ca6d726715313590c02e6ea34403" + integrity sha1-KspJayjEym1yZxUxNZDALm6jRAM= + lodash.isboolean@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" @@ -5520,6 +5570,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -7122,6 +7177,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +urijs@^1.19.10: + version "1.19.10" + resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.10.tgz#8e2fe70a8192845c180f75074884278f1eea26cb" + integrity sha512-EzauQlgKuJgsXOqoMrCiePBf4At5jVqRhXykF3Wfb8ZsOBMxPcfiVBcsHXug4Aepb/ICm2PIgqAUGMelgdrWEg== + urix@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"