This commit is contained in:
Luis Alvarez
2020-09-30 11:44:38 -05:00
commit eb44455cde
67 changed files with 19268 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
import {
CommerceAPI,
CommerceAPIOptions,
CommerceAPIFetchOptions,
} from 'lib/commerce/api';
import { GetAllProductsQuery } from '../schema';
import { getAllProductsQuery } from './operations/get-all-products';
type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>;
};
export interface GetAllProductsResult<T> {
products: T extends GetAllProductsQuery
? T['site']['products']['edges']
: unknown;
}
export default class BigcommerceAPI implements CommerceAPI {
protected commerceUrl: string;
protected apiToken: string;
constructor({ commerceUrl, apiToken }: CommerceAPIOptions) {
this.commerceUrl = commerceUrl;
this.apiToken = apiToken;
}
async fetch<T>(
query: string,
{ variables, preview }: CommerceAPIFetchOptions = {}
): Promise<T> {
const res = await fetch(this.commerceUrl + (preview ? '/preview' : ''), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiToken}`,
},
body: JSON.stringify({
query,
variables,
}),
});
const json = await res.json();
if (json.errors) {
console.error(json.errors);
throw new Error('Failed to fetch API');
}
return json.data;
}
async getAllProducts<T>(opts: {
query: string;
}): Promise<GetAllProductsResult<T>>;
async getAllProducts<T = GetAllProductsQuery>({
query,
}: { query?: string } = {}): Promise<
GetAllProductsResult<T | GetAllProductsQuery>
// T extends GetAllProductsQuery
// ? GetAllProductsResult<T['site']['products']['edges']>
// : Partial<GetAllProductsResult<any>>
> {
if (!query) {
const data = await this.fetch<GetAllProductsQuery>(getAllProductsQuery);
return {
products: data.site.products.edges,
};
}
return {
products: undefined,
};
}
}
let h = new BigcommerceAPI({ apiToken: '', commerceUrl: '' });
async function yay() {
const x = await h.getAllProducts<{ custom: 'val' }>({ query: 'yes' });
const y = await h.getAllProducts();
console.log(x.products);
}

View File

@@ -0,0 +1,79 @@
export const responsiveImageFragment = /* GraphQL */ `
fragment responsiveImage on Image {
url320wide: url(width: 320)
url640wide: url(width: 640)
url960wide: url(width: 960)
url1280wide: url(width: 1280)
}
`;
export const getAllProductsQuery = /* GraphQL */ `
query getAllProducts {
site {
products(first: 4) {
pageInfo {
startCursor
endCursor
}
edges {
cursor
node {
entityId
name
path
brand {
name
}
description
prices {
price {
value
currencyCode
}
salePrice {
value
currencyCode
}
}
images {
edges {
node {
...responsiveImage
}
}
}
variants {
edges {
node {
entityId
defaultImage {
...responsiveImage
}
}
}
}
options {
edges {
node {
entityId
displayName
isRequired
values {
edges {
node {
entityId
label
}
}
}
}
}
}
}
}
}
}
}
${responsiveImageFragment}
`;

14
lib/bigcommerce/cart.tsx Normal file
View File

@@ -0,0 +1,14 @@
import {
CartProvider as CommerceCartProvider,
useCart as useCommerceCart,
} from 'lib/commerce/cart';
export type Cart = any;
export function CartProvider({ children }) {
return <CommerceCartProvider>{children}</CommerceCartProvider>;
}
export function useCart() {
return useCommerceCart<Cart>();
}

47
lib/bigcommerce/index.tsx Normal file
View File

@@ -0,0 +1,47 @@
import {
CommerceProvider as CoreCommerceProvider,
Connector,
useCommerce as useCoreCommerce,
} from 'lib/commerce';
async function getText(res: Response) {
try {
return (await res.text()) || res.statusText;
} catch (error) {
return res.statusText;
}
}
async function getError(res: Response) {
if (res.headers.get('Content-Type')?.includes('application/json')) {
const data = await res.json();
return data.errors[0];
}
return { message: await getText(res) };
}
async function fetcher(url: string, query: string) {
const res = await fetch(url);
if (res.ok) {
return res.json();
}
throw await getError(res);
}
export const bigcommerce: Connector = {
locale: 'en-us',
fetcher,
};
// TODO: The connector should be extendable when a developer is using it
export function CommerceProvider({ children }) {
return (
<CoreCommerceProvider connector={bigcommerce}>
{children}
</CoreCommerceProvider>
);
}
export const useCommerce = () => useCoreCommerce();

1733
lib/bigcommerce/schema.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

105
lib/cart.js Normal file
View File

@@ -0,0 +1,105 @@
import { useState, useCallback } from "react";
import useSWR, { mutate } from "swr";
async function getText(res) {
try {
return (await res.text()) || res.statusText;
} catch (error) {
return res.statusText;
}
}
async function getError(res) {
if (res.headers.get("Content-Type")?.includes("application/json")) {
const data = await res.json();
return data.errors[0];
}
return { message: await getText(res) };
}
async function fetcher(url) {
const res = await fetch(url);
if (res.status === 200) {
return res.json();
}
throw await getError(res);
}
export function useCart() {
return useSWR("/api/cart", fetcher);
}
export function useAddToCart() {
const [{ addingToCart, error }, setStatus] = useState({
addingToCart: false,
});
const addToCart = useCallback(async ({ product }) => {
setStatus({ addingToCart: true });
const res = await fetch("/api/cart", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ product }),
});
// Product added as expected
if (res.status === 200) {
setStatus({ addingToCart: false });
return mutate("/api/cart");
}
const error = await getError(res);
console.error("Adding product to cart failed with:", res.status, error);
setStatus({ addingToCart: false, error });
}, []);
return { addToCart, addingToCart, error };
}
export function useUpdateCart() {
const [{ updatingCart, error }, setStatus] = useState({
updatingCart: false,
});
const updateCart = useCallback(async ({ product, item }) => {
setStatus({ updatingCart: true });
const res = await fetch(
`/api/cart?itemId=${item.id}`,
product.quantity < 1
? { method: "DELETE" }
: {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ product }),
}
);
// Product updated as expected
if (res.status === 200) {
setStatus({ updatingCart: false });
return mutate("/api/cart");
}
const error = await getError(res);
console.error("Update to cart failed with:", res.status, error);
setStatus({ updatingCart: false, error });
}, []);
return { updateCart, updatingCart, error };
}
export function useRemoveFromCart() {
const { updateCart, updatingCart, error } = useUpdateCart();
const removeFromCart = async ({ item }) => {
updateCart({ item, product: { quantity: 0 } });
};
return { removeFromCart, removingFromCart: updatingCart, error };
}

24
lib/commerce/api/index.ts Normal file
View File

@@ -0,0 +1,24 @@
export interface CommerceAPIOptions {
commerceUrl: string;
apiToken: string;
}
export interface CommerceAPIFetchOptions {
variables?: object;
preview?: boolean;
}
export interface CommerceAPI {
commerceUrl: string;
apiToken: string;
fetch<T>(query: string, queryData?: CommerceAPIFetchOptions): Promise<T>;
getAllProducts(options?: { query?: string }): Promise<any>;
}
// export default class CommerceAPI {
// getAllProducts(query: string) {
// }
// }

37
lib/commerce/cart.tsx Normal file
View File

@@ -0,0 +1,37 @@
import { createContext, useContext } from 'react';
import useSWR, { responseInterface } from 'swr';
import { useCommerce } from '.';
export type Cart = any;
export type CartResponse<C extends Cart> = responseInterface<C, Error> & {
isEmpty: boolean;
};
const CartContext = createContext<CartResponse<Cart>>(null);
function getCartCookie() {
// TODO: Figure how the cart should be persisted
return null;
}
export function CartProvider({ children }) {
const { hooks, fetcher } = useCommerce<Cart>();
const { useCart } = hooks;
const cartId = getCartCookie();
const response = useSWR(
() => (cartId ? [useCart.url, useCart.query, useCart.resolver] : null),
fetcher
);
const isEmpty = true;
return (
<CartContext.Provider value={{ ...response, isEmpty }}>
{children}
</CartContext.Provider>
);
}
export function useCart<C extends Cart>() {
return useContext(CartContext) as CartResponse<C>;
}

29
lib/commerce/index.tsx Normal file
View File

@@ -0,0 +1,29 @@
import { createContext, ReactNode, useContext } from 'react';
const Commerce = createContext<Connector>(null);
export type CommerceProps = {
children?: ReactNode;
connector: Connector;
};
export type Connector = {
fetcher: Fetcher<any>;
locale: string;
};
export type Fetcher<T> = (...args: any) => T | Promise<T>;
export function CommerceProvider({ children, connector }: CommerceProps) {
if (!connector) {
throw new Error(
'CommerceProvider requires a valid headless commerce connector'
);
}
return <Commerce.Provider value={connector}>{children}</Commerce.Provider>;
}
export function useCommerce<T extends Connector>() {
return useContext(Commerce) as T;
}