mirror of
https://github.com/vercel/commerce.git
synced 2025-07-22 20:26:49 +00:00
Changes
This commit is contained in:
84
lib/bigcommerce/api/index.ts
Normal file
84
lib/bigcommerce/api/index.ts
Normal 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);
|
||||
}
|
79
lib/bigcommerce/api/operations/get-all-products.ts
Normal file
79
lib/bigcommerce/api/operations/get-all-products.ts
Normal 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
14
lib/bigcommerce/cart.tsx
Normal 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
47
lib/bigcommerce/index.tsx
Normal 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
1733
lib/bigcommerce/schema.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1529
lib/bigcommerce/schema.graphql
Normal file
1529
lib/bigcommerce/schema.graphql
Normal file
File diff suppressed because it is too large
Load Diff
105
lib/cart.js
Normal file
105
lib/cart.js
Normal 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
24
lib/commerce/api/index.ts
Normal 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
37
lib/commerce/cart.tsx
Normal 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
29
lib/commerce/index.tsx
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user