Merge branch 'feature/m2-featured-product' of github.com:KieIO/grocery-vercel-commerce into feature/m2-add-product-to-cart

This commit is contained in:
DatNguyen
2021-10-04 14:20:07 +07:00
19 changed files with 530 additions and 42 deletions

View File

@@ -1,3 +1,4 @@
import { GetAllFacetsOperation } from './../types/facet';
import type { ServerResponse } from 'http'
import type { LoginOperation } from '../types/login'
import type { GetAllPagesOperation, GetPageOperation } from '../types/page'
@@ -23,6 +24,8 @@ export const OPERATIONS = [
'getAllProductPaths',
'getAllProducts',
'getProduct',
'getAllFacets',
] as const
export const defaultOperations = OPERATIONS.reduce((ops, k) => {
@@ -154,8 +157,27 @@ export type Operations<P extends APIProvider> = {
} & OperationOptions
): Promise<T['data']>
}
getAllFacets: {
<T extends GetAllFacetsOperation>(opts: {
variables?: T['variables']
config?: P['config']
preview?: boolean
}): Promise<T['data']>
<T extends GetAllFacetsOperation>(
opts: {
variables?: T['variables']
config?: P['config']
preview?: boolean
} & OperationOptions
): Promise<T['data']>
}
}
export type APIOperations<P extends APIProvider> = {
[K in keyof Operations<P>]?: (ctx: OperationContext<P>) => Operations<P>[K]
}

View File

@@ -15,6 +15,7 @@ Adding a commerce provider means adding a new folder in `framework` with a folde
- useSearch
- getProduct
- getAllProducts
- getAllFacets
- `wishlist`
- useWishlist
- useAddItem

View File

@@ -0,0 +1,52 @@
import { FacetValue } from './../../vendure/schema.d';
export type Facet = {
id: string
name: string
code: string
values: FacetValue[]
}
export type SearchFacetsBody = {
search?: string
sort?: string
locale?: string
}
export type FacetTypes = {
facet: Facet
searchBody: SearchFacetsBody
}
export type SearchFacetsHook<T extends FacetTypes = FacetTypes> = {
data: {
facets: T['facet'][]
found: boolean
}
body: T['searchBody']
input: T['searchBody']
fetcherInput: T['searchBody']
}
export type FacetsSchema<T extends FacetTypes = FacetTypes> = {
endpoint: {
options: {}
handlers: {
getFacets: SearchFacetsHook<T>
}
}
}
export type GetAllFacetsOperation<T extends FacetTypes = FacetTypes> = {
data: { facets: T['facet'][] }
variables: {
ids?: string[]
first?: number
}
}
export type GetFacetOperation<T extends FacetTypes = FacetTypes> = {
data: { facet?: T['facet'] }
variables: { code: string; } | { code?: never; }
}

View File

@@ -1,3 +1,5 @@
import { FacetValueFilterInput, LogicalOperator, SearchResultSortParameter } from "@framework/schema"
export type ProductImage = {
url: string
alt?: string
@@ -40,7 +42,6 @@ export type Product = {
slug?: string
path?: string
images: ProductImage[]
variants: ProductVariant[]
price: ProductPrice
options: ProductOption[]
}
@@ -79,17 +80,24 @@ export type ProductsSchema<T extends ProductTypes = ProductTypes> = {
export type GetAllProductPathsOperation<
T extends ProductTypes = ProductTypes
> = {
> = {
data: { products: Pick<T['product'], 'path'>[] }
variables: { first?: number }
}
}
export type GetAllProductsOperation<T extends ProductTypes = ProductTypes> = {
data: { products: T['product'][] }
variables: {
relevance?: 'featured' | 'best_selling' | 'newest'
ids?: string[]
first?: number
term?: String
facetValueIds?: string[]
facetValueOperator?: LogicalOperator
facetValueFilters?: FacetValueFilterInput[]
collectionId?: string
collectionSlug?: string
groupByProduct?: Boolean
take?: number
skip?: number
sort?: SearchResultSortParameter
}
}

View File

@@ -1,15 +1,16 @@
import type { APIProvider, CommerceAPIConfig } from '@commerce/api'
import type { CommerceAPIConfig } from '@commerce/api'
import { CommerceAPI, getCommerceApi as commerceApi } from '@commerce/api'
import fetchGraphqlApi from './utils/fetch-graphql-api'
import login from './operations/login'
import getAllFacets from './operations/get-all-facets'
import getAllPages from './operations/get-all-pages'
import getPage from './operations/get-page'
import getSiteInfo from './operations/get-site-info'
import getCustomerWishlist from './operations/get-customer-wishlist'
import getAllProductPaths from './operations/get-all-product-paths'
import getAllProducts from './operations/get-all-products'
import getCustomerWishlist from './operations/get-customer-wishlist'
import getPage from './operations/get-page'
import getProduct from './operations/get-product'
import getSiteInfo from './operations/get-site-info'
import login from './operations/login'
import fetchGraphqlApi from './utils/fetch-graphql-api'
export interface VendureConfig extends CommerceAPIConfig {}
@@ -40,6 +41,7 @@ const operations = {
getAllProductPaths,
getAllProducts,
getProduct,
getAllFacets,
}
export const provider = { config, operations }

View File

@@ -0,0 +1,45 @@
import { OperationContext } from '@commerce/api/operations'
import { Facet } from '@commerce/types/facet'
import { Provider, VendureConfig } from '../'
import { GetAllFacetsQuery } from '../../schema'
import { getAllFacetsQuery } from '../../utils/queries/get-all-facets-query'
export type FacetVariables = { first?: number }
export default function getAllFacetsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllFacets(opts?: {
variables?: FacetVariables
config?: Partial<VendureConfig>
preview?: boolean
}): Promise<{ facets: Facet[] }>
async function getAllFacets({
query = getAllFacetsQuery,
variables: { ...vars } = {},
config: cfg,
}: {
query?: string
variables?: FacetVariables
config?: Partial<VendureConfig>
preview?: boolean
} = {}): Promise<{ facets: Facet[] | any[] }> {
const config = commerce.getConfig(cfg)
const variables = {
input: {
take: vars.first,
groupByFacet: true,
},
}
const { data } = await config.fetch<GetAllFacetsQuery>(query, {
variables,
})
return {
facets: data.facets.items,
}
}
return getAllFacets
}

View File

@@ -5,7 +5,7 @@ import { normalizeSearchResult } from '../../utils/normalize'
import { getAllProductsQuery } from '../../utils/queries/get-all-products-query'
import { OperationContext } from '@commerce/api/operations'
export type ProductVariables = { first?: number }
export type ProductVariables = { first?: number, facetValueIds?: string[] }
export default function getAllProductsOperation({
commerce,
@@ -30,6 +30,7 @@ export default function getAllProductsOperation({
const variables = {
input: {
take: vars.first,
facetValueIds: vars.facetValueIds,
groupByProduct: true,
},
}

View File

@@ -93,6 +93,10 @@ export type QueryProductsArgs = {
options?: Maybe<ProductListOptions>
}
export type QueryFacetsArgs = {
options?: Maybe<FacetListOptions>
}
export type QuerySearchArgs = {
input: SearchInput
}
@@ -2727,6 +2731,13 @@ export type ProductListOptions = {
filter?: Maybe<ProductFilterParameter>
}
export type FacetListOptions = {
skip?: Maybe<Scalars['Int']>
take?: Maybe<Scalars['Int']>
sort?: Maybe<FacetSortParameter>
filter?: Maybe<FacetFilterParameter>
}
export type UpdateOrderItemsResult =
| Order
| OrderModificationError
@@ -2884,6 +2895,23 @@ export type ProductVariantSortParameter = {
discountPrice?: Maybe<SortOrder>
}
export type FacetFilterParameter = {
createdAt?: Maybe<DateOperators>
updatedAt?: Maybe<DateOperators>
languageCode?: Maybe<StringOperators>
name?: Maybe<StringOperators>
code?: Maybe<StringOperators>
}
export type FacetSortParameter = {
id?: Maybe<SortOrder>
createdAt?: Maybe<SortOrder>
updatedAt?: Maybe<SortOrder>
name?: Maybe<SortOrder>
code?: Maybe<SortOrder>
}
export type CustomerFilterParameter = {
createdAt?: Maybe<DateOperators>
updatedAt?: Maybe<DateOperators>
@@ -3008,7 +3036,9 @@ export type CartFragment = { __typename?: 'Order' } & Pick<
export type SearchResultFragment = { __typename?: 'SearchResult' } & Pick<
SearchResult,
'productId' | 'productName' | 'description' | 'slug' | 'sku' | 'currencyCode'
'productId' | 'sku' | 'productName' | 'description' | 'slug' | 'sku' | 'currencyCode'
| 'productAsset' | 'price' | 'priceWithTax' | 'currencyCode'
| 'collectionIds'
> & {
productAsset?: Maybe<
{ __typename?: 'SearchResultAsset' } & Pick<
@@ -3192,6 +3222,23 @@ export type GetAllProductsQuery = { __typename?: 'Query' } & {
}
}
export type GetAllFacetsQuery = { __typename?: 'Query' } & {
facets: { __typename?: 'FacetList' } & {
items: Array<
{ __typename?: 'Facet' } & Pick<
Facet,
'id' | 'name' | 'code'
> & {
parent?: Maybe<{ __typename?: 'Facet' } & Pick<Facet, 'id'>>
children?: Maybe<
Array<{ __typename?: 'Facet' } & Pick<Facet, 'id'>>
>
}
>,
'totalItems'
}
}
export type ActiveOrderQueryVariables = Exact<{ [key: string]: never }>
export type ActiveOrderQuery = { __typename?: 'Query' } & {

View File

@@ -3,18 +3,20 @@ import { Cart } from '@commerce/types/cart'
import { CartFragment, SearchResultFragment } from '../schema'
export function normalizeSearchResult(item: SearchResultFragment): Product {
const imageUrl = item.productAsset?.preview ? item.productAsset?.preview + '?w=800&mode=crop' : ''
return {
id: item.productId,
name: item.productName,
description: item.description,
slug: item.slug,
path: item.slug,
images: [{ url: item.productAsset?.preview + '?w=800&mode=crop' || '' }],
variants: [],
images: imageUrl ? [{ url: imageUrl }] : [],
price: {
// TODO: check price
value: (item.priceWithTax as any).min / 100,
currencyCode: item.currencyCode,
},
// TODO: check product option
options: [],
sku: item.sku,
}

View File

@@ -0,0 +1,17 @@
export const getAllFacetsQuery = /* GraphQL */ `
query facets ($options: FacetListOptions) {
facets (options: $options){
totalItems,
items {
id
name
code
values {
id
name
code
}
}
}
}
`

View File

@@ -1,8 +1,20 @@
import { ProductVariables } from '@framework/api/operations/get-all-products';
import { Product } from '@framework/schema';
import commerce from '@lib/api/commerce';
import { GetStaticPropsContext } from 'next';
import { Layout } from 'src/components/common';
import { FeaturedProductsCarousel, HomeBanner, HomeCategories, HomeCollection, HomeCTA, HomeFeature, HomeRecipe, HomeSubscribe, HomeVideo } from 'src/components/modules/home';
import HomeSpice from 'src/components/modules/home/HomeSpice/HomeSpice';
import { getAllFeaturedFacetId, getFreshProductFacetId } from 'src/utils/funtion.utils';
export default function Home() {
interface Props {
freshProducts: Product[],
featuredProducts: Product[],
}
export default function Home({ freshProducts, featuredProducts }: Props) {
console.log("total: ", freshProducts.length, featuredProducts.length)
console.log("rs: ", freshProducts, featuredProducts)
return (
<>
<HomeBanner />
@@ -10,8 +22,8 @@ export default function Home() {
<HomeCategories />
<HomeCollection />
<HomeVideo />
<HomeSpice/>
<FeaturedProductsCarousel/>
<HomeSpice />
<FeaturedProductsCarousel />
<HomeCTA />
<HomeRecipe />
<HomeSubscribe />
@@ -22,4 +34,59 @@ export default function Home() {
)
}
export async function getStaticProps({
preview,
locale,
locales,
}: GetStaticPropsContext) {
const config = { locale, locales }
const { facets } = await commerce.getAllFacets({
variables: {},
config,
preview,
})
const freshProductvariables: ProductVariables = {}
const freshFacetId = getFreshProductFacetId(facets)
if (freshFacetId) {
freshProductvariables.facetValueIds = [freshFacetId]
}
const freshProductsPromise = commerce.getAllProducts({
variables: freshProductvariables,
config,
preview,
})
const allFeaturedFacetId = getAllFeaturedFacetId(facets)
const featuredProductsPromise = commerce.getAllProducts({
variables: {
facetValueIds: allFeaturedFacetId
},
config,
preview,
})
try {
const rs = await Promise.all([freshProductsPromise, featuredProductsPromise])
return {
props: {
freshProducts: rs[0].products,
featuredProducts: rs[1].products
},
revalidate: 60,
}
} catch (err) {
}
}
Home.Layout = Layout

View File

@@ -4,6 +4,7 @@ import { ProductInfoDetail, ReleventProducts, ViewedProducts } from 'src/compone
import { BLOGS_DATA_TEST, INGREDIENT_DATA_TEST, RECIPE_DATA_TEST } from 'src/utils/demo-data'
export default function Slug() {
return <>
<ProductInfoDetail />
<RecipeDetail ingredients={INGREDIENT_DATA_TEST} />

View File

@@ -1,18 +1,51 @@
import { Layout } from 'src/components/common'
import { useMessage } from 'src/components/contexts'
export default function Test() {
const { showMessageError } = useMessage()
const handleClick = () => {
showMessageError("Create account successfully")
}
import commerce from '@lib/api/commerce';
import { GetStaticPropsContext } from 'next';
import { Layout } from 'src/components/common';
interface Props {
products: any
}
export default function Home({ products }: Props) {
return (
<>
<button onClick={handleClick}>Click me</button>
<p>
TOTAL: {products?.length}
</p>
{JSON.stringify(products[0])}
</>
)
}
Test.Layout = Layout
export async function getServerSideProps({
preview,
locale,
locales,
}: GetStaticPropsContext) {
const config = { locale, locales }
const productsPromise = commerce.getAllProducts({
// const productsPromise = commerce.getAllFacets({
variables: {
first: 70,
// filter: {
// name: {
// contains: 'ca'
// }
// }
},
config,
preview,
// Saleor provider only
...({ featured: true } as any),
})
const { products } = await productsPromise
return {
props: { products },
}
}
Home.Layout = Layout

View File

@@ -0,0 +1,3 @@
export { default as useFacets } from './useFacets'

View File

@@ -0,0 +1,11 @@
import { GetAllFacetsQuery, QueryFacetsArgs } from '@framework/schema'
import { getAllFacetsQuery } from '@framework/utils/queries/get-all-facets-query'
import gglFetcher from 'src/utils/gglFetcher'
import useSWR from 'swr'
const useFacets = (options?: QueryFacetsArgs) => {
const { data, isValidating, ...rest } = useSWR<GetAllFacetsQuery>([getAllFacetsQuery, options], gglFetcher)
return { items: data?.facets.items, totalItems: data?.facets.totalItems, loading: isValidating, ...rest }
}
export default useFacets

View File

@@ -0,0 +1,148 @@
import { Product } from '@framework/schema'
import React from 'react'
import { CollectionCarcousel } from '..'
import image5 from '../../../../../public/assets/images/image5.png'
import image6 from '../../../../../public/assets/images/image6.png'
import image7 from '../../../../../public/assets/images/image7.png'
import image8 from '../../../../../public/assets/images/image8.png'
interface FreshProductsProps {
data: Product[]
}
const dataTest = [
{
name: 'Tomato',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image5.src,
},
{
name: 'Cucumber',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image6.src,
},
{
name: 'Carrot',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image7.src,
},
{
name: 'Salad',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image8.src,
},
{
name: 'Tomato',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image5.src,
},
{
name: 'Cucumber',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image6.src,
},
{
name: 'Tomato',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image5.src,
},
{
name: 'Cucumber',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image6.src,
},
{
name: 'Carrot',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image7.src,
},
{
name: 'Salad',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image8.src,
},
{
name: 'Tomato',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image5.src,
},
{
name: 'Cucumber',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image6.src,
},
]
const FreshProducts = ({data}: FreshProductsProps) => {
return (
<div className="w-full">
<CollectionCarcousel
type="highlight"
data={data}
itemKey="product-1"
title="Fresh Products Today"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
category={"veggie"}
/>
<CollectionCarcousel
data={dataTest}
itemKey="product-2"
title="VEGGIE"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
category={"veggie"}
/>
<CollectionCarcousel
data={dataTest}
itemKey="product-3"
title="VEGGIE"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
category={"veggie"}
/>
<CollectionCarcousel
data={dataTest}
itemKey="product-4"
title="VEGGIE"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
category={"veggie"}
/>
<CollectionCarcousel
data={dataTest}
itemKey="product-5"
title="VEGGIE"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
category={"veggie"}
/>
<CollectionCarcousel
data={dataTest}
itemKey="product-6"
title="VEGGIE"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
category={"veggie"}
/>
</div>
)
}
export default FreshProducts

View File

@@ -4,6 +4,7 @@ export { default as HomeCategories } from './HomeCategories/HomeCategories'
export { default as HomeCTA } from './HomeCTA/HomeCTA'
export { default as HomeSubscribe } from './HomeSubscribe/HomeSubscribe'
export { default as HomeVideo } from './HomeVideo/HomeVideo'
export { default as FreshProducts } from './FreshProducts/FreshProducts'
export { default as HomeCollection } from './HomeCollection/HomeCollection'
export { default as HomeRecipe } from './HomeRecipe/HomeRecipe'
export { default as FeaturedProductsCarousel } from './FeaturedProductsCarousel/FeaturedProductsCarousel'

View File

@@ -108,6 +108,14 @@ export const CATEGORY = [
},
]
export const FACET = {
FEATURE: {
PARENT_NAME: 'Featured',
FRESH: 'Fresh',
BEST_SELLERS: 'Best seller'
}
}
export const FEATURED = [
{
name: 'Best Sellers',
@@ -141,3 +149,4 @@ export const STATE_OPTIONS = [
value: 'Hà Nội',
},
]

View File

@@ -1,3 +1,7 @@
import { FacetValue } from './../../framework/vendure/schema.d';
import { Facet } from "@commerce/types/facet";
import { FACET } from "./constanst.utils";
export function isMobile() {
return window.innerWidth < 768
}
@@ -9,3 +13,17 @@ export function removeItem<T>(arr: Array<T>, value: T): Array<T> {
}
return [...arr];
}
export function getFreshProductFacetId(facets: Facet[]) {
const featuredFacet = facets.find((item: Facet) => item.name === FACET.FEATURE.PARENT_NAME)
const freshFacetValue = featuredFacet?.values.find((item: FacetValue) => item.name === FACET.FEATURE.FRESH)
return freshFacetValue?.id
}
export function getAllFeaturedFacetId(facets: Facet[]) {
const featuredFacet = facets.find((item: Facet) => item.name === FACET.FEATURE.PARENT_NAME)
const rs = featuredFacet?.values.map((item: FacetValue) => item.id)
return rs
}