mirror of
https://github.com/vercel/commerce.git
synced 2025-05-22 09:26:59 +00:00
Saleor
This commit is contained in:
parent
112d51303f
commit
a8e49ee3f4
@ -1,5 +1,4 @@
|
||||
TWITTER_CREATOR="@vercel"
|
||||
TWITTER_SITE="https://nextjs.org/commerce"
|
||||
SITE_NAME="Next.js Commerce"
|
||||
SHOPIFY_STOREFRONT_ACCESS_TOKEN=
|
||||
SHOPIFY_STORE_DOMAIN=
|
||||
TWITTER_CREATOR="@getsaleor"
|
||||
TWITTER_SITE="https://saleor.io/"
|
||||
SITE_NAME="Next.js Commerce by Saleor"
|
||||
SALEOR_INSTANCE_URL=https://vercel.saleor.cloud/graphql/
|
||||
|
25
.graphqlrc.yml
Normal file
25
.graphqlrc.yml
Normal file
@ -0,0 +1,25 @@
|
||||
overwrite: true
|
||||
schema: '${SALEOR_INSTANCE_URL}'
|
||||
documents: 'lib/**/*.graphql'
|
||||
generates:
|
||||
lib/saleor/generated/:
|
||||
preset: 'client'
|
||||
config:
|
||||
defaultScalarType: 'unknown'
|
||||
useTypeImports: true
|
||||
dedupeFragments: true
|
||||
skipTypename: true
|
||||
scalars:
|
||||
_Any: 'unknown'
|
||||
Date: 'string'
|
||||
DateTime: 'string'
|
||||
Decimal: 'number'
|
||||
GenericScalar: 'unknown'
|
||||
JSON: 'unknown'
|
||||
JSONString: 'string'
|
||||
Metadata: 'Record<string, string>'
|
||||
PositiveDecimal: 'number'
|
||||
Upload: 'unknown'
|
||||
UUID: 'string'
|
||||
WeightScalar: 'number'
|
||||
plugins: []
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -5,5 +5,6 @@
|
||||
"source.fixAll": true,
|
||||
"source.organizeImports": true,
|
||||
"source.sortMembers": true
|
||||
}
|
||||
},
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import Prose from 'components/prose';
|
||||
import { getPage } from 'lib/shopify';
|
||||
import { getPage } from 'lib/saleor';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
@ -1,7 +1,8 @@
|
||||
// @ts-nocheck
|
||||
import { cookies } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { addToCart, removeFromCart, updateCart } from 'lib/shopify';
|
||||
import { addToCart, removeFromCart, updateCart } from 'lib/saleor';
|
||||
import { isShopifyError } from 'lib/type-guards';
|
||||
|
||||
function formatErrorMessage(err: Error): string {
|
||||
|
@ -10,7 +10,7 @@ import { Gallery } from 'components/product/gallery';
|
||||
import { VariantSelector } from 'components/product/variant-selector';
|
||||
import Prose from 'components/prose';
|
||||
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
|
||||
import { getProduct, getProductRecommendations } from 'lib/shopify';
|
||||
import { getProduct, getProductRecommendations } from 'lib/saleor';
|
||||
import { Image } from 'lib/types';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getCollection, getCollectionProducts } from 'lib/shopify';
|
||||
import { getCollection, getCollectionProducts } from 'lib/saleor';
|
||||
import { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Grid from 'components/grid';
|
||||
import ProductGridItems from 'components/layout/product-grid-items';
|
||||
import { defaultSort, sorting } from 'lib/constants';
|
||||
import { getProducts } from 'lib/shopify';
|
||||
import { getProducts } from 'lib/saleor';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getCollections, getPages, getProducts } from 'lib/shopify';
|
||||
import { getCollections, getPages, getProducts } from 'lib/saleor';
|
||||
import { MetadataRoute } from 'next';
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getCollectionProducts } from 'lib/shopify';
|
||||
import { getCollectionProducts } from 'lib/saleor';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createCart, getCart } from 'lib/shopify';
|
||||
import { createCart, getCart } from 'lib/saleor';
|
||||
import { cookies } from 'next/headers';
|
||||
import CartButton from './button';
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { GridTileImage } from 'components/grid/tile';
|
||||
import { getCollectionProducts } from 'lib/shopify';
|
||||
import { getCollectionProducts } from 'lib/saleor';
|
||||
import type { Product } from 'lib/types';
|
||||
import Link from 'next/link';
|
||||
|
||||
|
@ -3,7 +3,7 @@ import Link from 'next/link';
|
||||
import GitHubIcon from 'components/icons/github';
|
||||
import LogoIcon from 'components/icons/logo';
|
||||
import VercelIcon from 'components/icons/vercel';
|
||||
import { getMenu } from 'lib/shopify';
|
||||
import { getMenu } from 'lib/saleor';
|
||||
import { Menu } from 'lib/types';
|
||||
|
||||
const { SITE_NAME } = process.env;
|
||||
|
@ -1,10 +1,9 @@
|
||||
import Link from 'next/link';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import Cart from 'components/cart';
|
||||
import CartIcon from 'components/icons/cart';
|
||||
import LogoIcon from 'components/icons/logo';
|
||||
import { getMenu } from 'lib/shopify';
|
||||
import { getMenu } from 'lib/saleor';
|
||||
import { Menu } from 'lib/types';
|
||||
import MobileMenu from './mobile-menu';
|
||||
import Search from './search';
|
||||
@ -44,8 +43,8 @@ export default async function Navbar() {
|
||||
|
||||
<div className="flex w-1/3 justify-end">
|
||||
<Suspense fallback={<CartIcon className="h-6" />}>
|
||||
{/* @ts-expect-error Server Component */}
|
||||
<Cart />
|
||||
{/* @ts- expect-error Server Component */}
|
||||
{/* <Cart /> */}
|
||||
</Suspense>
|
||||
</div>
|
||||
</nav>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { getCollections } from 'lib/shopify';
|
||||
import { getCollections } from 'lib/saleor';
|
||||
import FilterList from './filter';
|
||||
|
||||
async function CollectionList() {
|
||||
|
@ -1,23 +1,40 @@
|
||||
import { ProductOrderField } from './saleor/generated/graphql';
|
||||
|
||||
export type SortFilterItem = {
|
||||
title: string;
|
||||
slug: string | null;
|
||||
sortKey: 'RELEVANCE' | 'BEST_SELLING' | 'CREATED_AT' | 'PRICE';
|
||||
sortKey: ProductOrderField;
|
||||
reverse: boolean;
|
||||
};
|
||||
|
||||
export const defaultSort: SortFilterItem = {
|
||||
title: 'Relevance',
|
||||
slug: null,
|
||||
sortKey: 'RELEVANCE',
|
||||
sortKey: ProductOrderField.Rank,
|
||||
reverse: false
|
||||
};
|
||||
|
||||
export const sorting: SortFilterItem[] = [
|
||||
defaultSort,
|
||||
{ title: 'Trending', slug: 'trending-desc', sortKey: 'BEST_SELLING', reverse: false }, // asc
|
||||
{ title: 'Latest arrivals', slug: 'latest-desc', sortKey: 'CREATED_AT', reverse: true },
|
||||
{ title: 'Price: Low to high', slug: 'price-asc', sortKey: 'PRICE', reverse: false }, // asc
|
||||
{ title: 'Price: High to low', slug: 'price-desc', sortKey: 'PRICE', reverse: true }
|
||||
{ title: 'Trending', slug: 'trending-desc', sortKey: ProductOrderField.Rating, reverse: false }, // asc
|
||||
{
|
||||
title: 'Latest arrivals',
|
||||
slug: 'latest-desc',
|
||||
sortKey: ProductOrderField.PublishedAt,
|
||||
reverse: true
|
||||
},
|
||||
{
|
||||
title: 'Price: Low to high',
|
||||
slug: 'price-asc',
|
||||
sortKey: ProductOrderField.MinimalPrice,
|
||||
reverse: false
|
||||
}, // asc
|
||||
{
|
||||
title: 'Price: High to low',
|
||||
slug: 'price-desc',
|
||||
sortKey: ProductOrderField.MinimalPrice,
|
||||
reverse: true
|
||||
}
|
||||
];
|
||||
|
||||
export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden';
|
||||
|
@ -2,8 +2,18 @@ fragment FeaturedProduct on Product {
|
||||
id
|
||||
slug
|
||||
name
|
||||
isAvailableForPurchase
|
||||
description
|
||||
seoTitle
|
||||
seoDescription
|
||||
pricing {
|
||||
priceRange {
|
||||
start {
|
||||
gross {
|
||||
currency
|
||||
amount
|
||||
}
|
||||
}
|
||||
stop {
|
||||
gross {
|
||||
currency
|
||||
@ -17,4 +27,20 @@ fragment FeaturedProduct on Product {
|
||||
type
|
||||
alt
|
||||
}
|
||||
collections {
|
||||
name
|
||||
}
|
||||
updatedAt
|
||||
variants {
|
||||
id
|
||||
name
|
||||
pricing {
|
||||
price {
|
||||
gross {
|
||||
currency
|
||||
amount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
fragment Menu on Menu {
|
||||
id
|
||||
name
|
||||
items {
|
||||
...MenuItem
|
||||
}
|
||||
}
|
||||
fragment MenuItem on MenuItem {
|
||||
id
|
||||
name
|
||||
url
|
||||
collection {
|
||||
slug
|
||||
}
|
||||
children {
|
||||
id
|
||||
collection {
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
48
lib/saleor/generated/fragment-masking.ts
Normal file
48
lib/saleor/generated/fragment-masking.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import type { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core';
|
||||
|
||||
export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> =
|
||||
TDocumentType extends DocumentTypeDecoration<infer TType, any>
|
||||
? TType extends { ' $fragmentName'?: infer TKey }
|
||||
? TKey extends string
|
||||
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
|
||||
: never
|
||||
: never
|
||||
: never;
|
||||
|
||||
// return non-nullable if `fragmentType` is non-nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
|
||||
): TType;
|
||||
// return nullable if `fragmentType` is nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
|
||||
): TType | null | undefined;
|
||||
// return array of non-nullable if `fragmentType` is array of non-nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
|
||||
): ReadonlyArray<TType>;
|
||||
// return array of nullable if `fragmentType` is array of nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
|
||||
): ReadonlyArray<TType> | null | undefined;
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType:
|
||||
| FragmentType<DocumentTypeDecoration<TType, any>>
|
||||
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
|
||||
| null
|
||||
| undefined
|
||||
): TType | ReadonlyArray<TType> | null | undefined {
|
||||
return fragmentType as any;
|
||||
}
|
||||
|
||||
export function makeFragmentData<
|
||||
F extends DocumentTypeDecoration<any, any>,
|
||||
FT extends ResultOf<F>
|
||||
>(data: FT, _fragment: F): FragmentType<F> {
|
||||
return data as FragmentType<F>;
|
||||
}
|
118
lib/saleor/generated/gql.ts
Normal file
118
lib/saleor/generated/gql.ts
Normal file
@ -0,0 +1,118 @@
|
||||
/* eslint-disable */
|
||||
import * as types from './graphql';
|
||||
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
||||
|
||||
/**
|
||||
* Map of all GraphQL operations in the project.
|
||||
*
|
||||
* This map has several performance disadvantages:
|
||||
* 1. It is not tree-shakeable, so it will include all operations in the project.
|
||||
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
|
||||
* 3. It does not support dead code elimination, so it will add unused operations.
|
||||
*
|
||||
* Therefore it is highly recommended to use the babel or swc plugin for production.
|
||||
*/
|
||||
const documents = {
|
||||
'fragment FeaturedProduct on Product {\n id\n slug\n name\n isAvailableForPurchase\n description\n seoTitle\n seoDescription\n pricing {\n priceRange {\n start {\n gross {\n currency\n amount\n }\n }\n stop {\n gross {\n currency\n amount\n }\n }\n }\n }\n media {\n url(size: 2160)\n type\n alt\n }\n collections {\n name\n }\n updatedAt\n variants {\n id\n name\n pricing {\n price {\n gross {\n currency\n amount\n }\n }\n }\n }\n}':
|
||||
types.FeaturedProductFragmentDoc,
|
||||
'query GetCollectionBySlug($slug: String!) {\n collection(channel: "default-channel", slug: $slug) {\n id\n name\n slug\n description\n seoTitle\n seoDescription\n }\n}':
|
||||
types.GetCollectionBySlugDocument,
|
||||
'query GetCollectionProductsBySlug($slug: String!) {\n collection(channel: "default-channel", slug: $slug) {\n products(first: 100) {\n edges {\n node {\n id\n slug\n name\n isAvailableForPurchase\n description\n seoTitle\n seoDescription\n pricing {\n priceRange {\n start {\n gross {\n currency\n amount\n }\n }\n stop {\n gross {\n currency\n amount\n }\n }\n }\n }\n media {\n url(size: 2160)\n type\n alt\n }\n collections {\n name\n }\n updatedAt\n variants {\n id\n name\n pricing {\n price {\n gross {\n currency\n amount\n }\n }\n }\n }\n }\n }\n }\n }\n}':
|
||||
types.GetCollectionProductsBySlugDocument,
|
||||
'query GetCollections {\n collections(channel: "default-channel", first: 100) {\n edges {\n node {\n id\n name\n slug\n description\n seoTitle\n seoDescription\n }\n }\n }\n}':
|
||||
types.GetCollectionsDocument,
|
||||
'query GetFeaturedProducts($first: Int!) {\n products(first: $first, channel: "default-channel") {\n edges {\n node {\n ...FeaturedProduct\n }\n }\n }\n}':
|
||||
types.GetFeaturedProductsDocument,
|
||||
'query GetMenuBySlug($slug: String!) {\n menu(slug: $slug, channel: "default-channel") {\n id\n slug\n name\n items {\n id\n name\n url\n collection {\n slug\n }\n children {\n id\n collection {\n slug\n }\n }\n }\n }\n}':
|
||||
types.GetMenuBySlugDocument,
|
||||
'query GetPageBySlug($slug: String!) {\n page(slug: $slug) {\n id\n title\n slug\n content\n seoTitle\n seoDescription\n created\n }\n}':
|
||||
types.GetPageBySlugDocument,
|
||||
'query GetProductBySlug($slug: String!) {\n product(slug: $slug) {\n id\n slug\n name\n isAvailableForPurchase\n description\n seoTitle\n seoDescription\n pricing {\n priceRange {\n start {\n gross {\n currency\n amount\n }\n }\n stop {\n gross {\n currency\n amount\n }\n }\n }\n }\n media {\n url(size: 2160)\n type\n alt\n }\n collections {\n name\n }\n updatedAt\n variants {\n id\n name\n pricing {\n price {\n gross {\n currency\n amount\n }\n }\n }\n }\n }\n}':
|
||||
types.GetProductBySlugDocument,
|
||||
'query SearchProducts($search: String!, $sortBy: ProductOrderField!, $sortDirection: OrderDirection!) {\n products(\n first: 100\n channel: "default-channel"\n sortBy: {field: $sortBy, direction: $sortDirection}\n filter: {search: $search}\n ) {\n edges {\n node {\n id\n slug\n name\n isAvailableForPurchase\n description\n seoTitle\n seoDescription\n pricing {\n priceRange {\n start {\n gross {\n currency\n amount\n }\n }\n stop {\n gross {\n currency\n amount\n }\n }\n }\n }\n media {\n url(size: 2160)\n type\n alt\n }\n collections {\n name\n }\n updatedAt\n variants {\n id\n name\n pricing {\n price {\n gross {\n currency\n amount\n }\n }\n }\n }\n }\n }\n }\n}':
|
||||
types.SearchProductsDocument,
|
||||
'query GetProducts {\n products(first: 10, channel: "default-channel") {\n edges {\n node {\n name\n }\n }\n }\n}':
|
||||
types.GetProductsDocument
|
||||
};
|
||||
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
|
||||
* ```
|
||||
*
|
||||
* The query argument is unknown!
|
||||
* Please regenerate the types.
|
||||
*/
|
||||
export function graphql(source: string): unknown;
|
||||
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: 'fragment FeaturedProduct on Product {\n id\n slug\n name\n isAvailableForPurchase\n description\n seoTitle\n seoDescription\n pricing {\n priceRange {\n start {\n gross {\n currency\n amount\n }\n }\n stop {\n gross {\n currency\n amount\n }\n }\n }\n }\n media {\n url(size: 2160)\n type\n alt\n }\n collections {\n name\n }\n updatedAt\n variants {\n id\n name\n pricing {\n price {\n gross {\n currency\n amount\n }\n }\n }\n }\n}'
|
||||
): (typeof documents)['fragment FeaturedProduct on Product {\n id\n slug\n name\n isAvailableForPurchase\n description\n seoTitle\n seoDescription\n pricing {\n priceRange {\n start {\n gross {\n currency\n amount\n }\n }\n stop {\n gross {\n currency\n amount\n }\n }\n }\n }\n media {\n url(size: 2160)\n type\n alt\n }\n collections {\n name\n }\n updatedAt\n variants {\n id\n name\n pricing {\n price {\n gross {\n currency\n amount\n }\n }\n }\n }\n}'];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: 'query GetCollectionBySlug($slug: String!) {\n collection(channel: "default-channel", slug: $slug) {\n id\n name\n slug\n description\n seoTitle\n seoDescription\n }\n}'
|
||||
): (typeof documents)['query GetCollectionBySlug($slug: String!) {\n collection(channel: "default-channel", slug: $slug) {\n id\n name\n slug\n description\n seoTitle\n seoDescription\n }\n}'];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: 'query GetCollectionProductsBySlug($slug: String!) {\n collection(channel: "default-channel", slug: $slug) {\n products(first: 100) {\n edges {\n node {\n id\n slug\n name\n isAvailableForPurchase\n description\n seoTitle\n seoDescription\n pricing {\n priceRange {\n start {\n gross {\n currency\n amount\n }\n }\n stop {\n gross {\n currency\n amount\n }\n }\n }\n }\n media {\n url(size: 2160)\n type\n alt\n }\n collections {\n name\n }\n updatedAt\n variants {\n id\n name\n pricing {\n price {\n gross {\n currency\n amount\n }\n }\n }\n }\n }\n }\n }\n }\n}'
|
||||
): (typeof documents)['query GetCollectionProductsBySlug($slug: String!) {\n collection(channel: "default-channel", slug: $slug) {\n products(first: 100) {\n edges {\n node {\n id\n slug\n name\n isAvailableForPurchase\n description\n seoTitle\n seoDescription\n pricing {\n priceRange {\n start {\n gross {\n currency\n amount\n }\n }\n stop {\n gross {\n currency\n amount\n }\n }\n }\n }\n media {\n url(size: 2160)\n type\n alt\n }\n collections {\n name\n }\n updatedAt\n variants {\n id\n name\n pricing {\n price {\n gross {\n currency\n amount\n }\n }\n }\n }\n }\n }\n }\n }\n}'];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: 'query GetCollections {\n collections(channel: "default-channel", first: 100) {\n edges {\n node {\n id\n name\n slug\n description\n seoTitle\n seoDescription\n }\n }\n }\n}'
|
||||
): (typeof documents)['query GetCollections {\n collections(channel: "default-channel", first: 100) {\n edges {\n node {\n id\n name\n slug\n description\n seoTitle\n seoDescription\n }\n }\n }\n}'];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: 'query GetFeaturedProducts($first: Int!) {\n products(first: $first, channel: "default-channel") {\n edges {\n node {\n ...FeaturedProduct\n }\n }\n }\n}'
|
||||
): (typeof documents)['query GetFeaturedProducts($first: Int!) {\n products(first: $first, channel: "default-channel") {\n edges {\n node {\n ...FeaturedProduct\n }\n }\n }\n}'];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: 'query GetMenuBySlug($slug: String!) {\n menu(slug: $slug, channel: "default-channel") {\n id\n slug\n name\n items {\n id\n name\n url\n collection {\n slug\n }\n children {\n id\n collection {\n slug\n }\n }\n }\n }\n}'
|
||||
): (typeof documents)['query GetMenuBySlug($slug: String!) {\n menu(slug: $slug, channel: "default-channel") {\n id\n slug\n name\n items {\n id\n name\n url\n collection {\n slug\n }\n children {\n id\n collection {\n slug\n }\n }\n }\n }\n}'];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: 'query GetPageBySlug($slug: String!) {\n page(slug: $slug) {\n id\n title\n slug\n content\n seoTitle\n seoDescription\n created\n }\n}'
|
||||
): (typeof documents)['query GetPageBySlug($slug: String!) {\n page(slug: $slug) {\n id\n title\n slug\n content\n seoTitle\n seoDescription\n created\n }\n}'];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: 'query GetProductBySlug($slug: String!) {\n product(slug: $slug) {\n id\n slug\n name\n isAvailableForPurchase\n description\n seoTitle\n seoDescription\n pricing {\n priceRange {\n start {\n gross {\n currency\n amount\n }\n }\n stop {\n gross {\n currency\n amount\n }\n }\n }\n }\n media {\n url(size: 2160)\n type\n alt\n }\n collections {\n name\n }\n updatedAt\n variants {\n id\n name\n pricing {\n price {\n gross {\n currency\n amount\n }\n }\n }\n }\n }\n}'
|
||||
): (typeof documents)['query GetProductBySlug($slug: String!) {\n product(slug: $slug) {\n id\n slug\n name\n isAvailableForPurchase\n description\n seoTitle\n seoDescription\n pricing {\n priceRange {\n start {\n gross {\n currency\n amount\n }\n }\n stop {\n gross {\n currency\n amount\n }\n }\n }\n }\n media {\n url(size: 2160)\n type\n alt\n }\n collections {\n name\n }\n updatedAt\n variants {\n id\n name\n pricing {\n price {\n gross {\n currency\n amount\n }\n }\n }\n }\n }\n}'];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: 'query SearchProducts($search: String!, $sortBy: ProductOrderField!, $sortDirection: OrderDirection!) {\n products(\n first: 100\n channel: "default-channel"\n sortBy: {field: $sortBy, direction: $sortDirection}\n filter: {search: $search}\n ) {\n edges {\n node {\n id\n slug\n name\n isAvailableForPurchase\n description\n seoTitle\n seoDescription\n pricing {\n priceRange {\n start {\n gross {\n currency\n amount\n }\n }\n stop {\n gross {\n currency\n amount\n }\n }\n }\n }\n media {\n url(size: 2160)\n type\n alt\n }\n collections {\n name\n }\n updatedAt\n variants {\n id\n name\n pricing {\n price {\n gross {\n currency\n amount\n }\n }\n }\n }\n }\n }\n }\n}'
|
||||
): (typeof documents)['query SearchProducts($search: String!, $sortBy: ProductOrderField!, $sortDirection: OrderDirection!) {\n products(\n first: 100\n channel: "default-channel"\n sortBy: {field: $sortBy, direction: $sortDirection}\n filter: {search: $search}\n ) {\n edges {\n node {\n id\n slug\n name\n isAvailableForPurchase\n description\n seoTitle\n seoDescription\n pricing {\n priceRange {\n start {\n gross {\n currency\n amount\n }\n }\n stop {\n gross {\n currency\n amount\n }\n }\n }\n }\n media {\n url(size: 2160)\n type\n alt\n }\n collections {\n name\n }\n updatedAt\n variants {\n id\n name\n pricing {\n price {\n gross {\n currency\n amount\n }\n }\n }\n }\n }\n }\n }\n}'];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: 'query GetProducts {\n products(first: 10, channel: "default-channel") {\n edges {\n node {\n name\n }\n }\n }\n}'
|
||||
): (typeof documents)['query GetProducts {\n products(first: 10, channel: "default-channel") {\n edges {\n node {\n name\n }\n }\n }\n}'];
|
||||
|
||||
export function graphql(source: string) {
|
||||
return (documents as any)[source] ?? {};
|
||||
}
|
||||
|
||||
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> =
|
||||
TDocumentNode extends DocumentNode<infer TType, any> ? TType : never;
|
26094
lib/saleor/generated/graphql.ts
Normal file
26094
lib/saleor/generated/graphql.ts
Normal file
File diff suppressed because it is too large
Load Diff
2
lib/saleor/generated/index.ts
Normal file
2
lib/saleor/generated/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './fragment-masking';
|
||||
export * from './gql';
|
378
lib/saleor/index.ts
Normal file
378
lib/saleor/index.ts
Normal file
@ -0,0 +1,378 @@
|
||||
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
||||
import { print } from 'graphql';
|
||||
import { Collection, Menu, Page, Product } from 'lib/types';
|
||||
import {
|
||||
GetCollectionBySlugDocument,
|
||||
GetCollectionProductsBySlugDocument,
|
||||
GetCollectionsDocument,
|
||||
GetMenuBySlugDocument,
|
||||
GetPageBySlugDocument,
|
||||
GetProductBySlugDocument,
|
||||
OrderDirection,
|
||||
ProductOrderField,
|
||||
SearchProductsDocument
|
||||
} from './generated/graphql';
|
||||
import { invariant } from './utils';
|
||||
|
||||
const endpoint = process.env.SALEOR_INSTANCE_URL;
|
||||
invariant(endpoint, `Missing SALEOR_INSTANCE_URL!`);
|
||||
|
||||
type GraphQlError = {
|
||||
message: string;
|
||||
};
|
||||
type GraphQlErrorRespone<T> = { data: T } | { errors: readonly GraphQlError[] };
|
||||
|
||||
export async function saleorFetch<Result, Variables>({
|
||||
query,
|
||||
variables,
|
||||
headers,
|
||||
cache = 'force-cache'
|
||||
}: {
|
||||
query: TypedDocumentNode<Result, Variables>;
|
||||
variables: Variables;
|
||||
headers?: HeadersInit;
|
||||
cache?: RequestCache;
|
||||
}): Promise<Result> {
|
||||
invariant(endpoint, `Missing SALEOR_INSTANCE_URL!`);
|
||||
|
||||
const result = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: print(query),
|
||||
...(variables && { variables })
|
||||
}),
|
||||
cache,
|
||||
next: { revalidate: 900 } // 15 minutes
|
||||
});
|
||||
|
||||
const body = (await result.json()) as GraphQlErrorRespone<Result>;
|
||||
|
||||
if ('errors' in body) {
|
||||
throw body.errors[0];
|
||||
}
|
||||
|
||||
return body.data;
|
||||
}
|
||||
|
||||
export async function getCollections(): Promise<Collection[]> {
|
||||
const saleorCollections = await saleorFetch({
|
||||
query: GetCollectionsDocument,
|
||||
variables: {}
|
||||
});
|
||||
|
||||
return (
|
||||
saleorCollections.collections?.edges.map((edge) => {
|
||||
return {
|
||||
handle: edge.node.slug,
|
||||
title: edge.node.name,
|
||||
description: edge.node.description as string,
|
||||
seo: {
|
||||
title: edge.node.seoTitle || edge.node.name,
|
||||
description: edge.node.seoDescription || ''
|
||||
},
|
||||
updatedAt: '', // @todo ?
|
||||
path: `/search/${edge.node.slug}`
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
export async function getPage(handle: string): Promise<Page> {
|
||||
const saleorPage = await saleorFetch({
|
||||
query: GetPageBySlugDocument,
|
||||
variables: {
|
||||
slug: handle
|
||||
}
|
||||
});
|
||||
|
||||
if (!saleorPage.page) {
|
||||
throw new Error(`Page not found: ${handle}`);
|
||||
}
|
||||
|
||||
return {
|
||||
id: saleorPage.page.id,
|
||||
title: saleorPage.page.title,
|
||||
handle: saleorPage.page.slug,
|
||||
body: saleorPage.page.content || '',
|
||||
bodySummary: saleorPage.page.seoDescription || '',
|
||||
seo: {
|
||||
title: saleorPage.page.seoTitle || saleorPage.page.title,
|
||||
description: saleorPage.page.seoDescription || ''
|
||||
},
|
||||
createdAt: saleorPage.page.created,
|
||||
updatedAt: saleorPage.page.created
|
||||
};
|
||||
}
|
||||
|
||||
export async function getProduct(handle: string): Promise<Product | undefined> {
|
||||
const saleorProduct = await saleorFetch({
|
||||
query: GetProductBySlugDocument,
|
||||
variables: {
|
||||
slug: handle
|
||||
}
|
||||
});
|
||||
|
||||
if (!saleorProduct.product) {
|
||||
throw new Error(`Product not found: ${handle}`);
|
||||
}
|
||||
|
||||
const images =
|
||||
saleorProduct.product.media
|
||||
?.filter((media) => media.type === 'IMAGE')
|
||||
.map((media) => {
|
||||
return {
|
||||
url: media.url,
|
||||
altText: media.alt,
|
||||
width: 2048,
|
||||
height: 2048
|
||||
};
|
||||
}) || [];
|
||||
|
||||
return {
|
||||
id: saleorProduct.product.id,
|
||||
handle: saleorProduct.product.slug,
|
||||
availableForSale: saleorProduct.product.isAvailableForPurchase || true,
|
||||
title: saleorProduct.product.name,
|
||||
description: saleorProduct.product.description || '',
|
||||
descriptionHtml: saleorProduct.product.description || '', // @todo
|
||||
options: [], // @todo
|
||||
priceRange: {
|
||||
maxVariantPrice: {
|
||||
amount: saleorProduct.product.pricing?.priceRange?.stop?.gross.amount.toString() || '0',
|
||||
currencyCode: saleorProduct.product.pricing?.priceRange?.stop?.gross.currency || ''
|
||||
},
|
||||
minVariantPrice: {
|
||||
amount: saleorProduct.product.pricing?.priceRange?.start?.gross.amount.toString() || '0',
|
||||
currencyCode: saleorProduct.product.pricing?.priceRange?.start?.gross.currency || ''
|
||||
}
|
||||
},
|
||||
variants:
|
||||
saleorProduct.product.variants?.map((variant) => {
|
||||
return {
|
||||
id: variant.id,
|
||||
title: variant.name,
|
||||
availableForSale: saleorProduct.product?.isAvailableForPurchase || true,
|
||||
selectedOptions: [], // @todo
|
||||
price: {
|
||||
amount: variant.pricing?.price?.gross.amount.toString() || '0',
|
||||
currencyCode: variant.pricing?.price?.gross.currency || ''
|
||||
}
|
||||
};
|
||||
}) || [],
|
||||
images: images,
|
||||
featuredImage: images[0]!,
|
||||
seo: {
|
||||
title: saleorProduct.product.seoTitle || saleorProduct.product.name,
|
||||
description: saleorProduct.product.seoDescription || ''
|
||||
},
|
||||
tags: saleorProduct.product.collections?.map((c) => c.name) || [],
|
||||
updatedAt: saleorProduct.product.updatedAt
|
||||
};
|
||||
}
|
||||
|
||||
export async function getCollection(handle: string): Promise<Collection | undefined> {
|
||||
const saleorCollection = await saleorFetch({
|
||||
query: GetCollectionBySlugDocument,
|
||||
variables: {
|
||||
slug: handle
|
||||
}
|
||||
});
|
||||
|
||||
if (!saleorCollection.collection) {
|
||||
throw new Error(`Collection not found: ${handle}`);
|
||||
}
|
||||
|
||||
return {
|
||||
handle: saleorCollection.collection.slug,
|
||||
title: saleorCollection.collection.name,
|
||||
description: saleorCollection.collection.description as string,
|
||||
seo: {
|
||||
title: saleorCollection.collection.seoTitle || saleorCollection.collection.name,
|
||||
description: saleorCollection.collection.seoDescription || ''
|
||||
},
|
||||
updatedAt: '', // @todo ?
|
||||
path: `/search/${saleorCollection.collection.slug}`
|
||||
};
|
||||
}
|
||||
|
||||
export async function getCollectionProducts(handle: string): Promise<Product[]> {
|
||||
const handleToSlug: Record<string, string> = {
|
||||
'hidden-homepage-featured-items': 'featured',
|
||||
'hidden-homepage-carousel': 'all-products'
|
||||
};
|
||||
|
||||
const saleorCollectionProducts = await saleorFetch({
|
||||
query: GetCollectionProductsBySlugDocument,
|
||||
variables: {
|
||||
slug: handleToSlug[handle] || handle
|
||||
}
|
||||
});
|
||||
|
||||
if (!saleorCollectionProducts.collection) {
|
||||
throw new Error(`Collection not found: ${handle}`);
|
||||
}
|
||||
|
||||
return (
|
||||
saleorCollectionProducts.collection.products?.edges.map((product) => {
|
||||
const images =
|
||||
product.node.media
|
||||
?.filter((media) => media.type === 'IMAGE')
|
||||
.map((media) => {
|
||||
return {
|
||||
url: media.url,
|
||||
altText: media.alt,
|
||||
width: 2048,
|
||||
height: 2048
|
||||
};
|
||||
}) || [];
|
||||
|
||||
return {
|
||||
id: product.node.id,
|
||||
handle: product.node.slug,
|
||||
availableForSale: product.node.isAvailableForPurchase || true,
|
||||
title: product.node.name,
|
||||
description: product.node.description || '',
|
||||
descriptionHtml: product.node.description || '', // @todo
|
||||
options: [], // @todo
|
||||
priceRange: {
|
||||
maxVariantPrice: {
|
||||
amount: product.node.pricing?.priceRange?.stop?.gross.amount.toString() || '0',
|
||||
currencyCode: product.node.pricing?.priceRange?.stop?.gross.currency || ''
|
||||
},
|
||||
minVariantPrice: {
|
||||
amount: product.node.pricing?.priceRange?.start?.gross.amount.toString() || '0',
|
||||
currencyCode: product.node.pricing?.priceRange?.start?.gross.currency || ''
|
||||
}
|
||||
},
|
||||
variants:
|
||||
product.node.variants?.map((variant) => {
|
||||
return {
|
||||
id: variant.id,
|
||||
title: variant.name,
|
||||
availableForSale: product.node?.isAvailableForPurchase || true,
|
||||
selectedOptions: [], // @todo
|
||||
price: {
|
||||
amount: variant.pricing?.price?.gross.amount.toString() || '0',
|
||||
currencyCode: variant.pricing?.price?.gross.currency || ''
|
||||
}
|
||||
};
|
||||
}) || [],
|
||||
images: images,
|
||||
featuredImage: images[0]!,
|
||||
seo: {
|
||||
title: product.node.seoTitle || product.node.name,
|
||||
description: product.node.seoDescription || ''
|
||||
},
|
||||
tags: product.node.collections?.map((c) => c.name) || [],
|
||||
updatedAt: product.node.updatedAt
|
||||
};
|
||||
}) || []
|
||||
);
|
||||
}
|
||||
|
||||
export async function getMenu(handle: string): Promise<Menu[]> {
|
||||
const handleToSlug: Record<string, string> = {
|
||||
'next-js-frontend-footer-menu': 'footer',
|
||||
'next-js-frontend-header-menu': 'navbar'
|
||||
};
|
||||
|
||||
const saleorMenu = await saleorFetch({
|
||||
query: GetMenuBySlugDocument,
|
||||
variables: {
|
||||
slug: handleToSlug[handle] || handle
|
||||
}
|
||||
});
|
||||
|
||||
if (!saleorMenu.menu) {
|
||||
throw new Error(`Menu not found: ${handle}`);
|
||||
}
|
||||
|
||||
return (
|
||||
saleorMenu.menu.items?.map((item) => {
|
||||
return {
|
||||
path: item.url || '', // @todo handle manus without url
|
||||
title: item.name
|
||||
};
|
||||
}) || []
|
||||
);
|
||||
}
|
||||
|
||||
export async function getProducts({
|
||||
query,
|
||||
reverse,
|
||||
sortKey
|
||||
}: {
|
||||
query?: string;
|
||||
reverse?: boolean;
|
||||
sortKey?: ProductOrderField;
|
||||
}): Promise<Product[]> {
|
||||
const saleorProducts = await saleorFetch({
|
||||
query: SearchProductsDocument,
|
||||
variables: {
|
||||
search: query || '',
|
||||
sortBy: sortKey || ProductOrderField.Rank,
|
||||
sortDirection: reverse ? OrderDirection.Desc : OrderDirection.Asc
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
saleorProducts.products?.edges.map((product) => {
|
||||
const images =
|
||||
product.node.media
|
||||
?.filter((media) => media.type === 'IMAGE')
|
||||
.map((media) => {
|
||||
return {
|
||||
url: media.url,
|
||||
altText: media.alt,
|
||||
width: 2048,
|
||||
height: 2048
|
||||
};
|
||||
}) || [];
|
||||
|
||||
return {
|
||||
id: product.node.id,
|
||||
handle: product.node.slug,
|
||||
availableForSale: product.node.isAvailableForPurchase || true,
|
||||
title: product.node.name,
|
||||
description: product.node.description || '',
|
||||
descriptionHtml: product.node.description || '', // @todo
|
||||
options: [], // @todo
|
||||
priceRange: {
|
||||
maxVariantPrice: {
|
||||
amount: product.node.pricing?.priceRange?.stop?.gross.amount.toString() || '0',
|
||||
currencyCode: product.node.pricing?.priceRange?.stop?.gross.currency || ''
|
||||
},
|
||||
minVariantPrice: {
|
||||
amount: product.node.pricing?.priceRange?.start?.gross.amount.toString() || '0',
|
||||
currencyCode: product.node.pricing?.priceRange?.start?.gross.currency || ''
|
||||
}
|
||||
},
|
||||
variants:
|
||||
product.node.variants?.map((variant) => {
|
||||
return {
|
||||
id: variant.id,
|
||||
title: variant.name,
|
||||
availableForSale: product.node?.isAvailableForPurchase || true,
|
||||
selectedOptions: [], // @todo
|
||||
price: {
|
||||
amount: variant.pricing?.price?.gross.amount.toString() || '0',
|
||||
currencyCode: variant.pricing?.price?.gross.currency || ''
|
||||
}
|
||||
};
|
||||
}) || [],
|
||||
images: images,
|
||||
featuredImage: images[0]!,
|
||||
seo: {
|
||||
title: product.node.seoTitle || product.node.name,
|
||||
description: product.node.seoDescription || ''
|
||||
},
|
||||
tags: product.node.collections?.map((c) => c.name) || [],
|
||||
updatedAt: product.node.updatedAt
|
||||
};
|
||||
}) || []
|
||||
);
|
||||
}
|
10
lib/saleor/queries/GetCollectionBySlug.graphql
Normal file
10
lib/saleor/queries/GetCollectionBySlug.graphql
Normal file
@ -0,0 +1,10 @@
|
||||
query GetCollectionBySlug($slug: String!) {
|
||||
collection(channel: "default-channel", slug: $slug) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
description
|
||||
seoTitle
|
||||
seoDescription
|
||||
}
|
||||
}
|
54
lib/saleor/queries/GetCollectionProductsBySlug.graphql
Normal file
54
lib/saleor/queries/GetCollectionProductsBySlug.graphql
Normal file
@ -0,0 +1,54 @@
|
||||
query GetCollectionProductsBySlug($slug: String!) {
|
||||
collection(channel: "default-channel", slug: $slug) {
|
||||
products(first: 100) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
slug
|
||||
name
|
||||
isAvailableForPurchase
|
||||
description
|
||||
seoTitle
|
||||
seoDescription
|
||||
pricing {
|
||||
priceRange {
|
||||
start {
|
||||
gross {
|
||||
currency
|
||||
amount
|
||||
}
|
||||
}
|
||||
stop {
|
||||
gross {
|
||||
currency
|
||||
amount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
media {
|
||||
url(size: 2160)
|
||||
type
|
||||
alt
|
||||
}
|
||||
collections {
|
||||
name
|
||||
}
|
||||
updatedAt
|
||||
variants {
|
||||
id
|
||||
name
|
||||
pricing {
|
||||
price {
|
||||
gross {
|
||||
currency
|
||||
amount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,9 @@ query GetCollections {
|
||||
id
|
||||
name
|
||||
slug
|
||||
description
|
||||
seoTitle
|
||||
seoDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +0,0 @@
|
||||
query GetMenu($name: String!) {
|
||||
menu(name: $name, channel: "default-channel") {
|
||||
...Menu
|
||||
}
|
||||
}
|
21
lib/saleor/queries/GetMenuBySlug.graphql
Normal file
21
lib/saleor/queries/GetMenuBySlug.graphql
Normal file
@ -0,0 +1,21 @@
|
||||
query GetMenuBySlug($slug: String!) {
|
||||
menu(slug: $slug, channel: "default-channel") {
|
||||
id
|
||||
slug
|
||||
name
|
||||
items {
|
||||
id
|
||||
name
|
||||
url
|
||||
collection {
|
||||
slug
|
||||
}
|
||||
children {
|
||||
id
|
||||
collection {
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
lib/saleor/queries/GetPageBySlug.graphql
Normal file
11
lib/saleor/queries/GetPageBySlug.graphql
Normal file
@ -0,0 +1,11 @@
|
||||
query GetPageBySlug($slug: String!) {
|
||||
page(slug: $slug) {
|
||||
id
|
||||
title
|
||||
slug
|
||||
content
|
||||
seoTitle
|
||||
seoDescription
|
||||
created
|
||||
}
|
||||
}
|
48
lib/saleor/queries/GetProductBySlug.graphql
Normal file
48
lib/saleor/queries/GetProductBySlug.graphql
Normal file
@ -0,0 +1,48 @@
|
||||
query GetProductBySlug($slug: String!) {
|
||||
product(slug: $slug) {
|
||||
id
|
||||
slug
|
||||
name
|
||||
isAvailableForPurchase
|
||||
description
|
||||
seoTitle
|
||||
seoDescription
|
||||
pricing {
|
||||
priceRange {
|
||||
start {
|
||||
gross {
|
||||
currency
|
||||
amount
|
||||
}
|
||||
}
|
||||
stop {
|
||||
gross {
|
||||
currency
|
||||
amount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
media {
|
||||
url(size: 2160)
|
||||
type
|
||||
alt
|
||||
}
|
||||
collections {
|
||||
name
|
||||
}
|
||||
updatedAt
|
||||
variants {
|
||||
id
|
||||
name
|
||||
pricing {
|
||||
price {
|
||||
gross {
|
||||
currency
|
||||
amount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,50 @@ query SearchProducts(
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
...FeaturedProduct
|
||||
id
|
||||
slug
|
||||
name
|
||||
isAvailableForPurchase
|
||||
description
|
||||
seoTitle
|
||||
seoDescription
|
||||
pricing {
|
||||
priceRange {
|
||||
start {
|
||||
gross {
|
||||
currency
|
||||
amount
|
||||
}
|
||||
}
|
||||
stop {
|
||||
gross {
|
||||
currency
|
||||
amount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
media {
|
||||
url(size: 2160)
|
||||
type
|
||||
alt
|
||||
}
|
||||
collections {
|
||||
name
|
||||
}
|
||||
updatedAt
|
||||
variants {
|
||||
id
|
||||
name
|
||||
pricing {
|
||||
price {
|
||||
gross {
|
||||
currency
|
||||
amount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
5
lib/saleor/utils.ts
Normal file
5
lib/saleor/utils.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export function invariant<T>(val: T | null | undefined, message: string): asserts val is T {
|
||||
if (val === undefined || val === null) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
@ -12,8 +12,7 @@ module.exports = {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'cdn.shopify.com',
|
||||
pathname: '/s/files/**'
|
||||
hostname: 'vercel.saleor.cloud'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -13,7 +13,8 @@
|
||||
"prettier": "prettier --write --ignore-unknown .",
|
||||
"prettier:check": "prettier --check --ignore-unknown .",
|
||||
"test": "pnpm lint && pnpm prettier:check",
|
||||
"test:e2e": "playwright test"
|
||||
"test:e2e": "playwright test",
|
||||
"codegen": "graphql-codegen -r dotenv/config --config .graphqlrc.yml"
|
||||
},
|
||||
"git": {
|
||||
"pre-commit": "lint-staged"
|
||||
@ -22,10 +23,12 @@
|
||||
"*": "prettier --write --ignore-unknown"
|
||||
},
|
||||
"dependencies": {
|
||||
"@graphql-typed-document-node/core": "3.2.0",
|
||||
"@headlessui/react": "^1.7.10",
|
||||
"@vercel/og": "^0.1.0",
|
||||
"clsx": "^1.2.1",
|
||||
"framer-motion": "^8.4.0",
|
||||
"graphql": "16.6.0",
|
||||
"is-empty-iterable": "^3.0.0",
|
||||
"next": "13.3.1",
|
||||
"react": "18.2.0",
|
||||
@ -33,6 +36,8 @@
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "3.3.1",
|
||||
"@graphql-codegen/client-preset": "3.0.1",
|
||||
"@playwright/test": "^1.31.2",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/node": "18.13.0",
|
||||
@ -40,6 +45,7 @@
|
||||
"@types/react-dom": "18.0.10",
|
||||
"@vercel/git-hooks": "^1.0.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"dotenv": "16.0.3",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-config-next": "^13.3.1",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
|
5272
pnpm-lock.yaml
generated
5272
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user