Static content done

This commit is contained in:
Michal Miszczyszyn 2023-05-02 11:15:45 +02:00
parent a8e49ee3f4
commit 8fc54664da
No known key found for this signature in database
13 changed files with 944 additions and 1436 deletions

View File

@ -4,11 +4,14 @@ documents: 'lib/**/*.graphql'
generates:
lib/saleor/generated/:
preset: 'client'
presetConfig:
fragmentMasking: false
config:
defaultScalarType: 'unknown'
useTypeImports: true
dedupeFragments: true
skipTypename: true
documentMode: 'string'
scalars:
_Any: 'unknown'
Date: 'string'

78
lib/saleor/editorjs.ts Normal file
View File

@ -0,0 +1,78 @@
import { invariant } from './utils';
type EditorJsHeaderBlock = {
type: 'header';
data: { text: string; level: 1 | 2 | 3 | 4 | 5 | 6 };
};
type EditorJsParagraphBlock = { type: 'paragraph'; data: { text: string } };
type EditorJsListBlock = {
type: 'list';
data: { style: 'unordered' | 'ordered'; items: string[] };
};
type EditorJsBlockCommon = { id: string };
type EditorJsBlocks = EditorJsHeaderBlock | EditorJsParagraphBlock | EditorJsListBlock;
type EditorJsBlock = EditorJsBlocks & EditorJsBlockCommon;
interface EditorJsResponse {
blocks: readonly EditorJsBlock[];
time: number;
version: string;
}
const parseEditorJson = (content: string): EditorJsResponse | null => {
try {
const data: EditorJsResponse = JSON.parse(content);
// manually validate it has more or less proper shape
invariant(data && 'blocks' in data, `Invalid shape`);
invariant(Array.isArray(data.blocks), `Invalid shape`);
invariant(
data.blocks.every((item) => 'type' in item && 'data' in item),
`Invalid shape`
);
return data;
} catch (err) {
console.error(err);
return null;
}
};
export const parseEditorJsToHtml = (content: string) => {
const data = parseEditorJson(content);
if (!data) {
return '';
}
const html = data.blocks
.map((block) => {
switch (block.type) {
case 'header':
return header(block.data);
case 'list':
return list(block.data);
case 'paragraph':
return paragraph(block.data);
default:
console.warn(`Unknown block type: ${JSON.stringify(block)}`);
return '';
}
})
.join('');
return html;
};
function list(data: EditorJsListBlock['data']): string {
const el = data.style === 'ordered' ? 'ul' : 'ol';
const items = data.items.map((item) => `<li>${item}</li>`).join('');
return `<${el}>${items}</${el}>`;
}
function paragraph({ text }: EditorJsParagraphBlock['data']): string {
return `<p>${text}</p>`;
}
function header({ level, text }: EditorJsHeaderBlock['data']): string {
return `<h${level}>${text}</h${level}>`;
}

View File

@ -1,6 +1,5 @@
/* 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.
@ -15,6 +14,10 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
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 GetCategoryBySlug($slug: String!) {\n category(slug: $slug) {\n id\n name\n slug\n description\n seoTitle\n seoDescription\n }\n}':
types.GetCategoryBySlugDocument,
'query GetCategoryProductsBySlug($slug: String!) {\n category(slug: $slug) {\n products(channel: "default-channel", 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.GetCategoryProductsBySlugDocument,
'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}':
@ -23,11 +26,13 @@ const documents = {
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,
'fragment MenuItem on MenuItem {\n id\n name\n url\n collection {\n slug\n }\n category {\n slug\n }\n page {\n slug\n }\n}\n\nquery GetMenuBySlug($slug: String!) {\n menu(slug: $slug, channel: "default-channel") {\n id\n slug\n name\n items {\n ...MenuItem\n children {\n ...MenuItem\n children {\n ...MenuItem\n children {\n ...MenuItem\n }\n }\n }\n }\n }\n}':
types.MenuItemFragmentDoc,
'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}':
'query GetPages {\n pages(first: 10) {\n edges {\n node {\n id\n title\n slug\n content\n seoTitle\n seoDescription\n created\n }\n }\n }\n}':
types.GetPagesDocument,
'query GetProductBySlug($slug: String!) {\n product(channel: "default-channel", 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,
@ -37,82 +42,83 @@ const documents = {
/**
* 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;
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 import('./graphql').FeaturedProductFragmentDoc;
/**
* 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}'];
source: 'query GetCategoryBySlug($slug: String!) {\n category(slug: $slug) {\n id\n name\n slug\n description\n seoTitle\n seoDescription\n }\n}'
): typeof import('./graphql').GetCategoryBySlugDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: 'query GetCategoryProductsBySlug($slug: String!) {\n category(slug: $slug) {\n products(channel: "default-channel", 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 import('./graphql').GetCategoryProductsBySlugDocument;
/**
* 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}'];
): typeof import('./graphql').GetCollectionBySlugDocument;
/**
* 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}'];
): typeof import('./graphql').GetCollectionProductsBySlugDocument;
/**
* 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}'];
): typeof import('./graphql').GetCollectionsDocument;
/**
* 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}'];
): typeof import('./graphql').GetFeaturedProductsDocument;
/**
* 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}'];
source: 'fragment MenuItem on MenuItem {\n id\n name\n url\n collection {\n slug\n }\n category {\n slug\n }\n page {\n slug\n }\n}\n\nquery GetMenuBySlug($slug: String!) {\n menu(slug: $slug, channel: "default-channel") {\n id\n slug\n name\n items {\n ...MenuItem\n children {\n ...MenuItem\n children {\n ...MenuItem\n children {\n ...MenuItem\n }\n }\n }\n }\n }\n}'
): typeof import('./graphql').MenuItemFragmentDoc;
/**
* 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}'];
): typeof import('./graphql').GetPageBySlugDocument;
/**
* 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}'];
source: 'query GetPages {\n pages(first: 10) {\n edges {\n node {\n id\n title\n slug\n content\n seoTitle\n seoDescription\n created\n }\n }\n }\n}'
): typeof import('./graphql').GetPagesDocument;
/**
* 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(channel: "default-channel", 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 import('./graphql').GetProductBySlugDocument;
/**
* 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}'];
): typeof import('./graphql').SearchProductsDocument;
/**
* 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}'];
): typeof import('./graphql').GetProductsDocument;
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;

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +1 @@
export * from './fragment-masking';
export * from './gql';

View File

@ -1,16 +1,20 @@
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
import { print } from 'graphql';
import { Collection, Menu, Page, Product } from 'lib/types';
import { Cart, Collection, Menu, Page, Product } from 'lib/types';
import { parseEditorJsToHtml } from './editorjs';
import {
GetCategoryBySlugDocument,
GetCategoryProductsBySlugDocument,
GetCollectionBySlugDocument,
GetCollectionProductsBySlugDocument,
GetCollectionsDocument,
GetMenuBySlugDocument,
GetPageBySlugDocument,
GetPagesDocument,
GetProductBySlugDocument,
MenuItemFragment,
OrderDirection,
ProductOrderField,
SearchProductsDocument
SearchProductsDocument,
TypedDocumentString
} from './generated/graphql';
import { invariant } from './utils';
@ -28,7 +32,7 @@ export async function saleorFetch<Result, Variables>({
headers,
cache = 'force-cache'
}: {
query: TypedDocumentNode<Result, Variables>;
query: TypedDocumentString<Result, Variables>;
variables: Variables;
headers?: HeadersInit;
cache?: RequestCache;
@ -42,7 +46,7 @@ export async function saleorFetch<Result, Variables>({
...headers
},
body: JSON.stringify({
query: print(query),
query: query.toString(),
...(variables && { variables })
}),
cache,
@ -138,7 +142,9 @@ export async function getProduct(handle: string): Promise<Product | undefined> {
availableForSale: saleorProduct.product.isAvailableForPurchase || true,
title: saleorProduct.product.name,
description: saleorProduct.product.description || '',
descriptionHtml: saleorProduct.product.description || '', // @todo
descriptionHtml: saleorProduct.product.description
? parseEditorJsToHtml(saleorProduct.product.description)
: '',
options: [], // @todo
priceRange: {
maxVariantPrice: {
@ -174,50 +180,78 @@ export async function getProduct(handle: string): Promise<Product | undefined> {
};
}
export async function getCollection(handle: string): Promise<Collection | undefined> {
const saleorCollection = await saleorFetch({
query: GetCollectionBySlugDocument,
variables: {
slug: handle
}
});
const _getCollection = async (handle: string) =>
(
await saleorFetch({
query: GetCollectionBySlugDocument,
variables: {
slug: handle
}
})
).collection;
const _getCategory = async (handle: string) =>
(
await saleorFetch({
query: GetCategoryBySlugDocument,
variables: {
slug: handle
}
})
).category;
if (!saleorCollection.collection) {
export async function getCollection(handle: string): Promise<Collection | undefined> {
const saleorCollection = (await _getCollection(handle)) || (await _getCategory(handle));
if (!saleorCollection) {
throw new Error(`Collection not found: ${handle}`);
}
return {
handle: saleorCollection.collection.slug,
title: saleorCollection.collection.name,
description: saleorCollection.collection.description as string,
handle: saleorCollection.slug,
title: saleorCollection.name,
description: saleorCollection.description as string,
seo: {
title: saleorCollection.collection.seoTitle || saleorCollection.collection.name,
description: saleorCollection.collection.seoDescription || ''
title: saleorCollection.seoTitle || saleorCollection.name,
description: saleorCollection.seoDescription || ''
},
updatedAt: '', // @todo ?
path: `/search/${saleorCollection.collection.slug}`
path: `/search/${saleorCollection.slug}`
};
}
const handleToSlug: Record<string, string> = {
'hidden-homepage-featured-items': 'featured',
'hidden-homepage-carousel': 'all-products'
};
const _getCollectionProducts = async (handle: string) =>
(
await saleorFetch({
query: GetCollectionProductsBySlugDocument,
variables: {
slug: handleToSlug[handle] || handle
}
})
).collection;
const _getCategoryProducts = async (handle: string) =>
(
await saleorFetch({
query: GetCategoryProductsBySlugDocument,
variables: {
slug: handleToSlug[handle] || handle
}
})
).category;
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 _getCollectionProducts(handle)) || (await _getCategoryProducts(handle));
const saleorCollectionProducts = await saleorFetch({
query: GetCollectionProductsBySlugDocument,
variables: {
slug: handleToSlug[handle] || handle
}
});
if (!saleorCollectionProducts.collection) {
if (!saleorCollectionProducts) {
throw new Error(`Collection not found: ${handle}`);
}
return (
saleorCollectionProducts.collection.products?.edges.map((product) => {
saleorCollectionProducts.products?.edges.map((product) => {
const images =
product.node.media
?.filter((media) => media.type === 'IMAGE')
@ -236,7 +270,9 @@ export async function getCollectionProducts(handle: string): Promise<Product[]>
availableForSale: product.node.isAvailableForPurchase || true,
title: product.node.name,
description: product.node.description || '',
descriptionHtml: product.node.description || '', // @todo
descriptionHtml: product.node.description
? parseEditorJsToHtml(product.node.description)
: '',
options: [], // @todo
priceRange: {
maxVariantPrice: {
@ -291,12 +327,47 @@ export async function getMenu(handle: string): Promise<Menu[]> {
throw new Error(`Menu not found: ${handle}`);
}
const result = flattenMenuItems(saleorMenu.menu.items);
return (
saleorMenu.menu.items?.map((item) => {
return {
path: item.url || '', // @todo handle manus without url
title: item.name
};
result
.filter(
(menu) =>
menu.path &&
// manually removing empty categories
// @todo ?
menu.path !== '/search/paints' &&
menu.path !== '/search/juices' &&
menu.path !== '/search/alcohol' &&
menu.path !== '/search/homewares' &&
menu.path !== '/search/groceries'
)
// unique by path
.filter((item1, idx, arr) => arr.findIndex((item2) => item2.path === item1.path) === idx)
.slice(0, 3)
);
}
type MenuItemWithChildren = MenuItemFragment & {
children?: null | undefined | MenuItemWithChildren[];
};
function flattenMenuItems(menuItems: null | undefined | MenuItemWithChildren[]): Menu[] {
return (
menuItems?.flatMap((item) => {
const path =
item.url ||
(item.collection
? `/search/${item.collection.slug}`
: item.category
? `/search/${item.category.slug}`
: '');
return [
{
path: path,
title: item.name
},
...flattenMenuItems(item.children)
];
}) || []
);
}
@ -339,7 +410,9 @@ export async function getProducts({
availableForSale: product.node.isAvailableForPurchase || true,
title: product.node.name,
description: product.node.description || '',
descriptionHtml: product.node.description || '', // @todo
descriptionHtml: product.node.description
? parseEditorJsToHtml(product.node.description)
: '',
options: [], // @todo
priceRange: {
maxVariantPrice: {
@ -376,3 +449,41 @@ export async function getProducts({
}) || []
);
}
export async function getPages(): Promise<Page[]> {
const saleorPages = await saleorFetch({
query: GetPagesDocument,
variables: {}
});
return (
saleorPages.pages?.edges.map((page) => {
return {
id: page.node.id,
title: page.node.title,
handle: page.node.slug,
body: page.node.content || '',
bodySummary: page.node.seoDescription || '',
seo: {
title: page.node.seoTitle || page.node.title,
description: page.node.seoDescription || ''
},
createdAt: page.node.created,
updatedAt: page.node.created
};
}) || []
);
}
export async function getCart(cartId: string): Promise<Cart | null> {
// @todo
return null;
}
export async function createCart(): Promise<Cart> {
// @todo
throw new Error(`Not implemented`);
}
export async function getProductRecommendations(productId: string): Promise<Product[]> {
// @todo
return [];
}

View File

@ -0,0 +1,10 @@
query GetCategoryBySlug($slug: String!) {
category(slug: $slug) {
id
name
slug
description
seoTitle
seoDescription
}
}

View File

@ -0,0 +1,54 @@
query GetCategoryProductsBySlug($slug: String!) {
category(slug: $slug) {
products(channel: "default-channel", 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
}
}
}
}
}
}
}
}
}

View File

@ -1,19 +1,32 @@
fragment MenuItem on MenuItem {
id
name
url
collection {
slug
}
category {
slug
}
page {
slug
}
}
query GetMenuBySlug($slug: String!) {
menu(slug: $slug, channel: "default-channel") {
id
slug
name
items {
id
name
url
collection {
slug
}
...MenuItem
children {
id
collection {
slug
...MenuItem
children {
...MenuItem
children {
...MenuItem
}
}
}
}

View File

@ -0,0 +1,15 @@
query GetPages {
pages(first: 10) {
edges {
node {
id
title
slug
content
seoTitle
seoDescription
created
}
}
}
}

View File

@ -1,5 +1,5 @@
query GetProductBySlug($slug: String!) {
product(slug: $slug) {
product(channel: "default-channel", slug: $slug) {
id
slug
name

View File

@ -23,12 +23,10 @@
"*": "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",

8
pnpm-lock.yaml generated
View File

@ -1,9 +1,6 @@
lockfileVersion: '6.0'
dependencies:
'@graphql-typed-document-node/core':
specifier: 3.2.0
version: registry.npmjs.org/@graphql-typed-document-node/core@3.2.0(graphql@16.6.0)
'@headlessui/react':
specifier: ^1.7.10
version: 1.7.14(react-dom@18.2.0)(react@18.2.0)
@ -16,9 +13,6 @@ dependencies:
framer-motion:
specifier: ^8.4.0
version: 8.5.5(react-dom@18.2.0)(react@18.2.0)
graphql:
specifier: 16.6.0
version: registry.npmjs.org/graphql@16.6.0
is-empty-iterable:
specifier: ^3.0.0
version: 3.0.0
@ -4923,6 +4917,7 @@ packages:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
dependencies:
graphql: registry.npmjs.org/graphql@16.6.0
dev: true
registry.npmjs.org/@jridgewell/gen-mapping@0.3.3:
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz}
@ -6080,6 +6075,7 @@ packages:
name: graphql
version: 16.6.0
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
dev: true
registry.npmjs.org/has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz}