mirror of
https://github.com/vercel/commerce.git
synced 2025-06-08 01:06:59 +00:00
Saleor
This commit is contained in:
parent
112d51303f
commit
a8e49ee3f4
@ -1,5 +1,4 @@
|
|||||||
TWITTER_CREATOR="@vercel"
|
TWITTER_CREATOR="@getsaleor"
|
||||||
TWITTER_SITE="https://nextjs.org/commerce"
|
TWITTER_SITE="https://saleor.io/"
|
||||||
SITE_NAME="Next.js Commerce"
|
SITE_NAME="Next.js Commerce by Saleor"
|
||||||
SHOPIFY_STOREFRONT_ACCESS_TOKEN=
|
SALEOR_INSTANCE_URL=https://vercel.saleor.cloud/graphql/
|
||||||
SHOPIFY_STORE_DOMAIN=
|
|
||||||
|
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.fixAll": true,
|
||||||
"source.organizeImports": true,
|
"source.organizeImports": true,
|
||||||
"source.sortMembers": true
|
"source.sortMembers": true
|
||||||
}
|
},
|
||||||
|
"editor.formatOnSave": true
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
import Prose from 'components/prose';
|
import Prose from 'components/prose';
|
||||||
import { getPage } from 'lib/shopify';
|
import { getPage } from 'lib/saleor';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
// @ts-nocheck
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
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';
|
import { isShopifyError } from 'lib/type-guards';
|
||||||
|
|
||||||
function formatErrorMessage(err: Error): string {
|
function formatErrorMessage(err: Error): string {
|
||||||
|
@ -10,7 +10,7 @@ import { Gallery } from 'components/product/gallery';
|
|||||||
import { VariantSelector } from 'components/product/variant-selector';
|
import { VariantSelector } from 'components/product/variant-selector';
|
||||||
import Prose from 'components/prose';
|
import Prose from 'components/prose';
|
||||||
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
|
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
|
||||||
import { getProduct, getProductRecommendations } from 'lib/shopify';
|
import { getProduct, getProductRecommendations } from 'lib/saleor';
|
||||||
import { Image } from 'lib/types';
|
import { Image } from 'lib/types';
|
||||||
|
|
||||||
export const runtime = 'edge';
|
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 { Metadata } from 'next';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Grid from 'components/grid';
|
import Grid from 'components/grid';
|
||||||
import ProductGridItems from 'components/layout/product-grid-items';
|
import ProductGridItems from 'components/layout/product-grid-items';
|
||||||
import { defaultSort, sorting } from 'lib/constants';
|
import { defaultSort, sorting } from 'lib/constants';
|
||||||
import { getProducts } from 'lib/shopify';
|
import { getProducts } from 'lib/saleor';
|
||||||
|
|
||||||
export const runtime = 'edge';
|
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';
|
import { MetadataRoute } from 'next';
|
||||||
|
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL
|
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 Image from 'next/image';
|
||||||
import Link from 'next/link';
|
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 { cookies } from 'next/headers';
|
||||||
import CartButton from './button';
|
import CartButton from './button';
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { GridTileImage } from 'components/grid/tile';
|
import { GridTileImage } from 'components/grid/tile';
|
||||||
import { getCollectionProducts } from 'lib/shopify';
|
import { getCollectionProducts } from 'lib/saleor';
|
||||||
import type { Product } from 'lib/types';
|
import type { Product } from 'lib/types';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import Link from 'next/link';
|
|||||||
import GitHubIcon from 'components/icons/github';
|
import GitHubIcon from 'components/icons/github';
|
||||||
import LogoIcon from 'components/icons/logo';
|
import LogoIcon from 'components/icons/logo';
|
||||||
import VercelIcon from 'components/icons/vercel';
|
import VercelIcon from 'components/icons/vercel';
|
||||||
import { getMenu } from 'lib/shopify';
|
import { getMenu } from 'lib/saleor';
|
||||||
import { Menu } from 'lib/types';
|
import { Menu } from 'lib/types';
|
||||||
|
|
||||||
const { SITE_NAME } = process.env;
|
const { SITE_NAME } = process.env;
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
import Cart from 'components/cart';
|
|
||||||
import CartIcon from 'components/icons/cart';
|
import CartIcon from 'components/icons/cart';
|
||||||
import LogoIcon from 'components/icons/logo';
|
import LogoIcon from 'components/icons/logo';
|
||||||
import { getMenu } from 'lib/shopify';
|
import { getMenu } from 'lib/saleor';
|
||||||
import { Menu } from 'lib/types';
|
import { Menu } from 'lib/types';
|
||||||
import MobileMenu from './mobile-menu';
|
import MobileMenu from './mobile-menu';
|
||||||
import Search from './search';
|
import Search from './search';
|
||||||
@ -45,7 +44,7 @@ export default async function Navbar() {
|
|||||||
<div className="flex w-1/3 justify-end">
|
<div className="flex w-1/3 justify-end">
|
||||||
<Suspense fallback={<CartIcon className="h-6" />}>
|
<Suspense fallback={<CartIcon className="h-6" />}>
|
||||||
{/* @ts- expect-error Server Component */}
|
{/* @ts- expect-error Server Component */}
|
||||||
<Cart />
|
{/* <Cart /> */}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
import { getCollections } from 'lib/shopify';
|
import { getCollections } from 'lib/saleor';
|
||||||
import FilterList from './filter';
|
import FilterList from './filter';
|
||||||
|
|
||||||
async function CollectionList() {
|
async function CollectionList() {
|
||||||
|
@ -1,23 +1,40 @@
|
|||||||
|
import { ProductOrderField } from './saleor/generated/graphql';
|
||||||
|
|
||||||
export type SortFilterItem = {
|
export type SortFilterItem = {
|
||||||
title: string;
|
title: string;
|
||||||
slug: string | null;
|
slug: string | null;
|
||||||
sortKey: 'RELEVANCE' | 'BEST_SELLING' | 'CREATED_AT' | 'PRICE';
|
sortKey: ProductOrderField;
|
||||||
reverse: boolean;
|
reverse: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultSort: SortFilterItem = {
|
export const defaultSort: SortFilterItem = {
|
||||||
title: 'Relevance',
|
title: 'Relevance',
|
||||||
slug: null,
|
slug: null,
|
||||||
sortKey: 'RELEVANCE',
|
sortKey: ProductOrderField.Rank,
|
||||||
reverse: false
|
reverse: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sorting: SortFilterItem[] = [
|
export const sorting: SortFilterItem[] = [
|
||||||
defaultSort,
|
defaultSort,
|
||||||
{ title: 'Trending', slug: 'trending-desc', sortKey: 'BEST_SELLING', reverse: false }, // asc
|
{ title: 'Trending', slug: 'trending-desc', sortKey: ProductOrderField.Rating, 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: 'Latest arrivals',
|
||||||
{ title: 'Price: High to low', slug: 'price-desc', sortKey: 'PRICE', reverse: true }
|
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';
|
export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden';
|
||||||
|
@ -2,8 +2,18 @@ fragment FeaturedProduct on Product {
|
|||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
name
|
name
|
||||||
|
isAvailableForPurchase
|
||||||
|
description
|
||||||
|
seoTitle
|
||||||
|
seoDescription
|
||||||
pricing {
|
pricing {
|
||||||
priceRange {
|
priceRange {
|
||||||
|
start {
|
||||||
|
gross {
|
||||||
|
currency
|
||||||
|
amount
|
||||||
|
}
|
||||||
|
}
|
||||||
stop {
|
stop {
|
||||||
gross {
|
gross {
|
||||||
currency
|
currency
|
||||||
@ -17,4 +27,20 @@ fragment FeaturedProduct on Product {
|
|||||||
type
|
type
|
||||||
alt
|
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
|
id
|
||||||
name
|
name
|
||||||
slug
|
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 {
|
edges {
|
||||||
node {
|
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: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
protocol: 'https',
|
protocol: 'https',
|
||||||
hostname: 'cdn.shopify.com',
|
hostname: 'vercel.saleor.cloud'
|
||||||
pathname: '/s/files/**'
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,8 @@
|
|||||||
"prettier": "prettier --write --ignore-unknown .",
|
"prettier": "prettier --write --ignore-unknown .",
|
||||||
"prettier:check": "prettier --check --ignore-unknown .",
|
"prettier:check": "prettier --check --ignore-unknown .",
|
||||||
"test": "pnpm lint && pnpm prettier:check",
|
"test": "pnpm lint && pnpm prettier:check",
|
||||||
"test:e2e": "playwright test"
|
"test:e2e": "playwright test",
|
||||||
|
"codegen": "graphql-codegen -r dotenv/config --config .graphqlrc.yml"
|
||||||
},
|
},
|
||||||
"git": {
|
"git": {
|
||||||
"pre-commit": "lint-staged"
|
"pre-commit": "lint-staged"
|
||||||
@ -22,10 +23,12 @@
|
|||||||
"*": "prettier --write --ignore-unknown"
|
"*": "prettier --write --ignore-unknown"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@graphql-typed-document-node/core": "3.2.0",
|
||||||
"@headlessui/react": "^1.7.10",
|
"@headlessui/react": "^1.7.10",
|
||||||
"@vercel/og": "^0.1.0",
|
"@vercel/og": "^0.1.0",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"framer-motion": "^8.4.0",
|
"framer-motion": "^8.4.0",
|
||||||
|
"graphql": "16.6.0",
|
||||||
"is-empty-iterable": "^3.0.0",
|
"is-empty-iterable": "^3.0.0",
|
||||||
"next": "13.3.1",
|
"next": "13.3.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
@ -33,6 +36,8 @@
|
|||||||
"react-dom": "18.2.0"
|
"react-dom": "18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@graphql-codegen/cli": "3.3.1",
|
||||||
|
"@graphql-codegen/client-preset": "3.0.1",
|
||||||
"@playwright/test": "^1.31.2",
|
"@playwright/test": "^1.31.2",
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"@types/node": "18.13.0",
|
"@types/node": "18.13.0",
|
||||||
@ -40,6 +45,7 @@
|
|||||||
"@types/react-dom": "18.0.10",
|
"@types/react-dom": "18.0.10",
|
||||||
"@vercel/git-hooks": "^1.0.0",
|
"@vercel/git-hooks": "^1.0.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
|
"dotenv": "16.0.3",
|
||||||
"eslint": "^8.35.0",
|
"eslint": "^8.35.0",
|
||||||
"eslint-config-next": "^13.3.1",
|
"eslint-config-next": "^13.3.1",
|
||||||
"eslint-config-prettier": "^8.6.0",
|
"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