mirror of
https://github.com/vercel/commerce.git
synced 2025-05-09 11:17:50 +00:00
some more cleanup
This commit is contained in:
parent
a4d2102933
commit
fbf42b7551
@ -43,7 +43,7 @@ Integrations enable upgraded or additional functionality for Next.js Commerce
|
|||||||
|
|
||||||
You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js Commerce. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables) for this, but a `.env` file is all that is necessary.
|
You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js Commerce. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables) for this, but a `.env` file is all that is necessary.
|
||||||
|
|
||||||
> Note: You should not commit your `.env` file or it will expose secrets that will allow others to control your Shopify store.
|
> Note: You should not commit your `.env` file or it will expose secrets that will allow others to control your Fourthwall store.
|
||||||
|
|
||||||
1. Install Vercel CLI: `npm i -g vercel`
|
1. Install Vercel CLI: `npm i -g vercel`
|
||||||
2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link`
|
2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link`
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
import { revalidate } from 'lib/shopify';
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
export async function POST(req: NextRequest): Promise<NextResponse> {
|
|
||||||
return revalidate(req);
|
|
||||||
}
|
|
@ -6,7 +6,7 @@ import { getCart } from 'lib/fourthwall';
|
|||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
description: 'High-performance ecommerce store built with Next.js, Vercel, and Shopify.',
|
description: 'High-performance ecommerce store built with Next.js, Vercel, and Fourthwall.',
|
||||||
openGraph: {
|
openGraph: {
|
||||||
type: 'website'
|
type: 'website'
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
import { GridTileImage } from 'components/grid/tile';
|
|
||||||
import Footer from 'components/layout/footer';
|
import Footer from 'components/layout/footer';
|
||||||
import { Gallery } from 'components/product/gallery';
|
import { Gallery } from 'components/product/gallery';
|
||||||
import { ProductProvider } from 'components/product/product-context';
|
import { ProductProvider } from 'components/product/product-context';
|
||||||
import { ProductDescription } from 'components/product/product-description';
|
import { ProductDescription } from 'components/product/product-description';
|
||||||
import { Wrapper } from 'components/wrapper';
|
import { Wrapper } from 'components/wrapper';
|
||||||
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
|
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
|
||||||
import { getCart, getProduct, getProductRecommendations } from 'lib/fourthwall';
|
import { getCart, getProduct } from 'lib/fourthwall';
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import Link from 'next/link';
|
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
@ -26,8 +24,8 @@ export async function generateMetadata({
|
|||||||
const indexable = !product.tags.includes(HIDDEN_PRODUCT_TAG);
|
const indexable = !product.tags.includes(HIDDEN_PRODUCT_TAG);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: product.seo.title || product.title,
|
title: product.title,
|
||||||
description: product.seo.description || product.description,
|
description: product.description,
|
||||||
robots: {
|
robots: {
|
||||||
index: indexable,
|
index: indexable,
|
||||||
follow: indexable,
|
follow: indexable,
|
||||||
@ -110,48 +108,9 @@ export default async function ProductPage({ params, searchParams }: { params: {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<RelatedProducts id={product.id} />
|
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</ProductProvider>
|
</ProductProvider>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function RelatedProducts({ id }: { id: string }) {
|
|
||||||
const relatedProducts = await getProductRecommendations(id);
|
|
||||||
|
|
||||||
if (!relatedProducts.length) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="py-8">
|
|
||||||
<h2 className="mb-4 text-2xl font-bold">Related Products</h2>
|
|
||||||
<ul className="flex w-full gap-4 overflow-x-auto pt-1">
|
|
||||||
{relatedProducts.map((product) => (
|
|
||||||
<li
|
|
||||||
key={product.handle}
|
|
||||||
className="aspect-square w-full flex-none min-[475px]:w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/5"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
className="relative h-full w-full"
|
|
||||||
href={`/product/${product.handle}`}
|
|
||||||
prefetch={true}
|
|
||||||
>
|
|
||||||
<GridTileImage
|
|
||||||
alt={product.title}
|
|
||||||
label={{
|
|
||||||
title: product.title,
|
|
||||||
amount: product.priceRange.maxVariantPrice.amount,
|
|
||||||
currencyCode: product.priceRange.maxVariantPrice.currencyCode
|
|
||||||
}}
|
|
||||||
src={product.featuredImage?.url}
|
|
||||||
fill
|
|
||||||
sizes="(min-width: 1024px) 20vw, (min-width: 768px) 25vw, (min-width: 640px) 33vw, (min-width: 475px) 50vw, 100vw"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { getCollections, getPages, getProducts } from 'lib/shopify';
|
import { getCollectionProducts, getCollections } from 'lib/fourthwall';
|
||||||
import { validateEnvironmentVariables } from 'lib/utils';
|
import { validateEnvironmentVariables } from 'lib/utils';
|
||||||
import { MetadataRoute } from 'next';
|
import { MetadataRoute } from 'next';
|
||||||
|
|
||||||
@ -21,31 +21,21 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|||||||
lastModified: new Date().toISOString()
|
lastModified: new Date().toISOString()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const collectionsPromise = getCollections().then((collections) =>
|
const collections = await getCollections();
|
||||||
collections.map((collection) => ({
|
|
||||||
url: `${baseUrl}${collection.path}`,
|
|
||||||
lastModified: collection.updatedAt
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
const productsPromise = getProducts({}).then((products) =>
|
const productsPromises = collections.map((collection) => {
|
||||||
products.map((product) => ({
|
return getCollectionProducts({ collection: collection.handle, currency: 'USD' }).then((products) =>
|
||||||
url: `${baseUrl}/product/${product.handle}`,
|
products.map((product) => ({
|
||||||
lastModified: product.updatedAt
|
url: `${baseUrl}/product/${product.handle}`,
|
||||||
}))
|
lastModified: product.updatedAt
|
||||||
);
|
}))
|
||||||
|
);
|
||||||
const pagesPromise = getPages().then((pages) =>
|
});
|
||||||
pages.map((page) => ({
|
|
||||||
url: `${baseUrl}/${page.handle}`,
|
|
||||||
lastModified: page.updatedAt
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
let fetchedRoutes: Route[] = [];
|
let fetchedRoutes: Route[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fetchedRoutes = (await Promise.all([collectionsPromise, productsPromise, pagesPromise])).flat();
|
fetchedRoutes = (await Promise.all([...productsPromises])).flat();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw JSON.stringify(error, null, 2);
|
throw JSON.stringify(error, null, 2);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import FooterMenu from 'components/layout/footer-menu';
|
|
||||||
import LogoSquare from 'components/logo-square';
|
import LogoSquare from 'components/logo-square';
|
||||||
import { getMenu } from 'lib/fourthwall';
|
|
||||||
import { Suspense } from 'react';
|
|
||||||
|
|
||||||
const { COMPANY_NAME, SITE_NAME } = process.env;
|
const { COMPANY_NAME, SITE_NAME } = process.env;
|
||||||
|
|
||||||
@ -11,7 +8,6 @@ export default async function Footer() {
|
|||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
const copyrightDate = 2023 + (currentYear > 2023 ? `-${currentYear}` : '');
|
const copyrightDate = 2023 + (currentYear > 2023 ? `-${currentYear}` : '');
|
||||||
const skeleton = 'w-full h-6 animate-pulse rounded bg-neutral-200 dark:bg-neutral-700';
|
const skeleton = 'w-full h-6 animate-pulse rounded bg-neutral-200 dark:bg-neutral-700';
|
||||||
const menu = await getMenu('next-js-frontend-footer-menu');
|
|
||||||
const copyrightName = COMPANY_NAME || SITE_NAME || '';
|
const copyrightName = COMPANY_NAME || SITE_NAME || '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -23,20 +19,6 @@ export default async function Footer() {
|
|||||||
<span className="uppercase">{SITE_NAME}</span>
|
<span className="uppercase">{SITE_NAME}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
<div className="flex h-[188px] w-[200px] flex-col gap-2">
|
|
||||||
<div className={skeleton} />
|
|
||||||
<div className={skeleton} />
|
|
||||||
<div className={skeleton} />
|
|
||||||
<div className={skeleton} />
|
|
||||||
<div className={skeleton} />
|
|
||||||
<div className={skeleton} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FooterMenu menu={menu} />
|
|
||||||
</Suspense>
|
|
||||||
<div className="md:ml-auto">
|
<div className="md:ml-auto">
|
||||||
<a
|
<a
|
||||||
className="flex h-8 w-max flex-none items-center justify-center rounded-md border border-neutral-200 bg-white text-xs text-black dark:border-neutral-700 dark:bg-black dark:text-white"
|
className="flex h-8 w-max flex-none items-center justify-center rounded-md border border-neutral-200 bg-white text-xs text-black dark:border-neutral-700 dark:bg-black dark:text-white"
|
||||||
@ -57,11 +39,11 @@ export default async function Footer() {
|
|||||||
</p>
|
</p>
|
||||||
<hr className="mx-4 hidden h-4 w-[1px] border-l border-neutral-400 md:inline-block" />
|
<hr className="mx-4 hidden h-4 w-[1px] border-l border-neutral-400 md:inline-block" />
|
||||||
<p>
|
<p>
|
||||||
<a href="https://github.com/vercel/commerce">View the source</a>
|
<a href="https://github.com/FourthwallHQ/vercel-commerce">View the source</a>
|
||||||
</p>
|
</p>
|
||||||
<p className="md:ml-auto">
|
<p className="md:ml-auto">
|
||||||
<a href="https://vercel.com" className="text-black dark:text-white">
|
<a href="https://fourthwall.com" className="text-black dark:text-white">
|
||||||
Created by ▲ Vercel
|
Created by Fourthwall
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,7 +16,7 @@ export function WelcomeToast() {
|
|||||||
},
|
},
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
This is a high-performance, SSR storefront powered by Shopify, Next.js, and Vercel.{' '}
|
This is a high-performance, SSR storefront powered by Fourthwall, Next.js, and Vercel.{' '}
|
||||||
<a
|
<a
|
||||||
href="https://vercel.com/templates/next.js/nextjs-commerce"
|
href="https://vercel.com/templates/next.js/nextjs-commerce"
|
||||||
className="text-blue-600 hover:underline"
|
className="text-blue-600 hover:underline"
|
||||||
|
@ -28,4 +28,4 @@ export const TAGS = {
|
|||||||
|
|
||||||
export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden';
|
export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden';
|
||||||
export const DEFAULT_OPTION = 'Default Title';
|
export const DEFAULT_OPTION = 'Default Title';
|
||||||
export const SHOPIFY_GRAPHQL_API_ENDPOINT = '/api/2023-01/graphql.json';
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Cart, Menu, Product } from "lib/types";
|
import { Cart, Collection, Product } from "lib/types";
|
||||||
import { reshapeCart, reshapeProduct, reshapeProducts } from "./reshape";
|
import { reshapeCart, reshapeProduct, reshapeProducts } from "./reshape";
|
||||||
import { FourthwallCart, FourthwallCheckout, FourthwallProduct } from "./types";
|
import { FourthwallCart, FourthwallCheckout, FourthwallCollection, FourthwallProduct } from "./types";
|
||||||
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_FW_API_URL;
|
const API_URL = process.env.NEXT_PUBLIC_FW_API_URL;
|
||||||
const FW_PUBLIC_TOKEN = process.env.NEXT_PUBLIC_FW_PUBLIC_TOKEN;
|
const FW_PUBLIC_TOKEN = process.env.NEXT_PUBLIC_FW_PUBLIC_TOKEN;
|
||||||
@ -68,16 +68,26 @@ async function fourthwallPost<T>(url: string, data: any, options: RequestInit =
|
|||||||
/**
|
/**
|
||||||
* Collection operations
|
* Collection operations
|
||||||
*/
|
*/
|
||||||
|
export async function getCollections(): Promise<Collection[]> {
|
||||||
|
const res = await fourthwallGet<{ results: FourthwallCollection[] }>(`${API_URL}/api/public/v1.0/collections`, {
|
||||||
|
headers: {
|
||||||
|
'X-ShopId': process.env.FW_SHOPID || ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.body.results.map((collection) => ({
|
||||||
|
handle: collection.slug,
|
||||||
|
title: collection.name,
|
||||||
|
description: collection.description,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
export async function getCollectionProducts({
|
export async function getCollectionProducts({
|
||||||
collection,
|
collection,
|
||||||
currency,
|
currency,
|
||||||
reverse,
|
|
||||||
sortKey
|
|
||||||
}: {
|
}: {
|
||||||
collection: string;
|
collection: string;
|
||||||
currency: string;
|
currency: string;
|
||||||
reverse?: boolean;
|
|
||||||
sortKey?: string;
|
|
||||||
}): Promise<Product[]> {
|
}): Promise<Product[]> {
|
||||||
const res = await fourthwallGet<{results: FourthwallProduct[]}>(`${API_URL}/api/public/v1.0/collections/${collection}/products?¤cy=${currency}`, {
|
const res = await fourthwallGet<{results: FourthwallProduct[]}>(`${API_URL}/api/public/v1.0/collections/${collection}/products?¤cy=${currency}`, {
|
||||||
headers: {
|
headers: {
|
||||||
@ -98,17 +108,11 @@ export async function getCollectionProducts({
|
|||||||
* Product operations
|
* Product operations
|
||||||
*/
|
*/
|
||||||
export async function getProduct({ handle, currency } : { handle: string, currency: string }): Promise<Product | undefined> {
|
export async function getProduct({ handle, currency } : { handle: string, currency: string }): Promise<Product | undefined> {
|
||||||
// TODO: replace with real URL
|
|
||||||
const res = await fourthwallGet<FourthwallProduct>(`${API_URL}/api/public/v1.0/products/${handle}?¤cy=${currency}`);
|
const res = await fourthwallGet<FourthwallProduct>(`${API_URL}/api/public/v1.0/products/${handle}?¤cy=${currency}`);
|
||||||
|
|
||||||
return reshapeProduct(res.body);
|
return reshapeProduct(res.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProductRecommendations(productId: string): Promise<Product[]> {
|
|
||||||
// TODO: replace with real URL
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cart operations
|
* Cart operations
|
||||||
*/
|
*/
|
||||||
@ -125,7 +129,7 @@ export async function getCart(cartId: string | undefined, currency: string): Pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createCart(): Promise<Cart> {
|
export async function createCart(): Promise<Cart> {
|
||||||
const res = await fourthwallPost<FourthwallCart>(`https://api.staging.fourthwall.com/api/public/v1.0/carts`, {
|
const res = await fourthwallPost<FourthwallCart>(`${API_URL}/api/public/v1.0/carts`, {
|
||||||
items: []
|
items: []
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -194,10 +198,3 @@ export async function createCheckout(
|
|||||||
|
|
||||||
return res.body;
|
return res.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: Stubbed out
|
|
||||||
*/
|
|
||||||
export async function getMenu(handle: string): Promise<Menu[]> {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
@ -53,6 +53,8 @@ export const reshapeProduct = (product: FourthwallProduct): Product | undefined
|
|||||||
const sizes = new Set(variants.map((v) => v.attributes.size.name));
|
const sizes = new Set(variants.map((v) => v.attributes.size.name));
|
||||||
const colors = new Set(variants.map((v) => v.attributes.color.name));
|
const colors = new Set(variants.map((v) => v.attributes.color.name));
|
||||||
|
|
||||||
|
const reshapedVariants = reshapeVariants(variants);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
handle: product.slug,
|
handle: product.slug,
|
||||||
@ -60,7 +62,7 @@ export const reshapeProduct = (product: FourthwallProduct): Product | undefined
|
|||||||
descriptionHtml: product.description,
|
descriptionHtml: product.description,
|
||||||
description: product.description,
|
description: product.description,
|
||||||
images: reshapeImages(images, product.name),
|
images: reshapeImages(images, product.name),
|
||||||
variants: reshapeVariants(variants),
|
variants: reshapedVariants,
|
||||||
priceRange: {
|
priceRange: {
|
||||||
minVariantPrice: {
|
minVariantPrice: {
|
||||||
amount: minPrice.toString(),
|
amount: minPrice.toString(),
|
||||||
@ -81,12 +83,7 @@ export const reshapeProduct = (product: FourthwallProduct): Product | undefined
|
|||||||
name: 'Size',
|
name: 'Size',
|
||||||
values: [...sizes]
|
values: [...sizes]
|
||||||
}],
|
}],
|
||||||
// TODO: stubbed out
|
availableForSale: reshapedVariants.some((v) => v.availableForSale),
|
||||||
availableForSale: true,
|
|
||||||
seo: {
|
|
||||||
title: product.name,
|
|
||||||
description: product.description,
|
|
||||||
},
|
|
||||||
tags: [],
|
tags: [],
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
@ -106,7 +103,7 @@ const reshapeVariants = (variants: FourthwallProductVariant[]): ProductVariant[]
|
|||||||
return variants.map((v) => ({
|
return variants.map((v) => ({
|
||||||
id: v.id,
|
id: v.id,
|
||||||
title: v.name,
|
title: v.name,
|
||||||
availableForSale: true,
|
availableForSale: v.stock.type === 'UNLIMITED' || (v.stock.inStock || 0) > 0,
|
||||||
images: reshapeImages(v.images, v.name),
|
images: reshapeImages(v.images, v.name),
|
||||||
selectedOptions: [{
|
selectedOptions: [{
|
||||||
name: 'Size',
|
name: 'Size',
|
||||||
|
@ -3,6 +3,14 @@ export type FourthwallMoney = {
|
|||||||
currency: string;
|
currency: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FourthwallCollection = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
description: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type FourthwallProduct = {
|
export type FourthwallProduct = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -11,6 +19,8 @@ export type FourthwallProduct = {
|
|||||||
|
|
||||||
images: FourthwallProductImage[];
|
images: FourthwallProductImage[];
|
||||||
variants: FourthwallProductVariant[];
|
variants: FourthwallProductVariant[];
|
||||||
|
|
||||||
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FourthwallProductImage = {
|
export type FourthwallProductImage = {
|
||||||
@ -28,6 +38,11 @@ export type FourthwallProductVariant = {
|
|||||||
|
|
||||||
images: FourthwallProductImage[];
|
images: FourthwallProductImage[];
|
||||||
|
|
||||||
|
stock: {
|
||||||
|
type: 'UNLIMITED' | 'LIMITED';
|
||||||
|
inStock?: number;
|
||||||
|
}
|
||||||
|
|
||||||
// other attr
|
// other attr
|
||||||
attributes: {
|
attributes: {
|
||||||
description: string;
|
description: string;
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
export interface ShopifyErrorLike {
|
|
||||||
status: number;
|
|
||||||
message: Error;
|
|
||||||
cause?: Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isObject = (object: unknown): object is Record<string, unknown> => {
|
|
||||||
return typeof object === 'object' && object !== null && !Array.isArray(object);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isShopifyError = (error: unknown): error is ShopifyErrorLike => {
|
|
||||||
if (!isObject(error)) return false;
|
|
||||||
|
|
||||||
if (error instanceof Error) return true;
|
|
||||||
|
|
||||||
return findError(error);
|
|
||||||
};
|
|
||||||
|
|
||||||
function findError<T extends object>(error: T): boolean {
|
|
||||||
if (Object.prototype.toString.call(error) === '[object Error]') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prototype = Object.getPrototypeOf(error) as T | null;
|
|
||||||
|
|
||||||
return prototype === null ? false : findError(prototype);
|
|
||||||
}
|
|
35
lib/types.ts
35
lib/types.ts
@ -1,13 +1,3 @@
|
|||||||
export type Maybe<T> = T | null;
|
|
||||||
|
|
||||||
export type Connection<T> = {
|
|
||||||
edges: Array<Edge<T>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Edge<T> = {
|
|
||||||
node: T;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Cart = {
|
export type Cart = {
|
||||||
id: string | undefined;
|
id: string | undefined;
|
||||||
checkoutUrl: string;
|
checkoutUrl: string;
|
||||||
@ -48,9 +38,6 @@ export type Collection = {
|
|||||||
handle: string;
|
handle: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
seo: SEO;
|
|
||||||
updatedAt: string;
|
|
||||||
path: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Image = {
|
export type Image = {
|
||||||
@ -60,27 +47,11 @@ export type Image = {
|
|||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Menu = {
|
|
||||||
title: string;
|
|
||||||
path: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Money = {
|
export type Money = {
|
||||||
amount: string;
|
amount: string;
|
||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Page = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
handle: string;
|
|
||||||
body: string;
|
|
||||||
bodySummary: string;
|
|
||||||
seo?: SEO;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Product = {
|
export type Product = {
|
||||||
id: string;
|
id: string;
|
||||||
handle: string;
|
handle: string;
|
||||||
@ -94,7 +65,6 @@ export type Product = {
|
|||||||
minVariantPrice: Money;
|
minVariantPrice: Money;
|
||||||
};
|
};
|
||||||
featuredImage: Image;
|
featuredImage: Image;
|
||||||
seo: SEO;
|
|
||||||
tags: string[];
|
tags: string[];
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
variants: ProductVariant[];
|
variants: ProductVariant[];
|
||||||
@ -118,8 +88,3 @@ export type ProductVariant = {
|
|||||||
price: Money;
|
price: Money;
|
||||||
images: Image[];
|
images: Image[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SEO = {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
};
|
|
||||||
|
13
lib/utils.ts
13
lib/utils.ts
@ -11,7 +11,7 @@ export const ensureStartsWith = (stringToCheck: string, startsWith: string) =>
|
|||||||
stringToCheck.startsWith(startsWith) ? stringToCheck : `${startsWith}${stringToCheck}`;
|
stringToCheck.startsWith(startsWith) ? stringToCheck : `${startsWith}${stringToCheck}`;
|
||||||
|
|
||||||
export const validateEnvironmentVariables = () => {
|
export const validateEnvironmentVariables = () => {
|
||||||
const requiredEnvironmentVariables = ['SHOPIFY_STORE_DOMAIN', 'SHOPIFY_STOREFRONT_ACCESS_TOKEN'];
|
const requiredEnvironmentVariables = ['NEXT_PUBLIC_FW_API_URL', 'NEXT_PUBLIC_FW_PUBLIC_TOKEN', 'NEXT_PUBLIC_FW_COLLECTION', 'NEXT_PUBLIC_FW_CHECKOUT'];
|
||||||
const missingEnvironmentVariables = [] as string[];
|
const missingEnvironmentVariables = [] as string[];
|
||||||
|
|
||||||
requiredEnvironmentVariables.forEach((envVar) => {
|
requiredEnvironmentVariables.forEach((envVar) => {
|
||||||
@ -22,18 +22,9 @@ export const validateEnvironmentVariables = () => {
|
|||||||
|
|
||||||
if (missingEnvironmentVariables.length) {
|
if (missingEnvironmentVariables.length) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The following environment variables are missing. Your site will not work without them. Read more: https://vercel.com/docs/integrations/shopify#configure-environment-variables\n\n${missingEnvironmentVariables.join(
|
`The following environment variables are missing. Your site will not work without them. Read more: https://vercel.com/docs/integrations/fourthwall#configure-environment-variables\n\n${missingEnvironmentVariables.join(
|
||||||
'\n'
|
'\n'
|
||||||
)}\n`
|
)}\n`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
process.env.SHOPIFY_STORE_DOMAIN?.includes('[') ||
|
|
||||||
process.env.SHOPIFY_STORE_DOMAIN?.includes(']')
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
'Your `SHOPIFY_STORE_DOMAIN` environment variable includes brackets (ie. `[` and / or `]`). Your site will not work with them there. Please remove them.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
@ -3,11 +3,6 @@ module.exports = {
|
|||||||
images: {
|
images: {
|
||||||
formats: ['image/avif', 'image/webp'],
|
formats: ['image/avif', 'image/webp'],
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
|
||||||
protocol: 'https',
|
|
||||||
hostname: 'cdn.shopify.com',
|
|
||||||
pathname: '/s/files/**'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
protocol: 'https',
|
protocol: 'https',
|
||||||
hostname: 'storage.googleapis.com',
|
hostname: 'storage.googleapis.com',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user