Merge branch 'release-stable' of https://github.com/KieIO/grocery-vercel-commerce into feature/m2-get-edit-user-info

This commit is contained in:
Quangnhankie
2021-10-05 11:52:33 +07:00
34 changed files with 920 additions and 282 deletions

View File

@@ -2,8 +2,8 @@
"features": {
"cart": true,
"search": true,
"wishlist": false,
"customerAuth": false,
"customCheckout": false
"wishlist": true,
"customerAuth": true,
"customCheckout": true
}
}

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'
@@ -9,6 +10,7 @@ import type {
GetProductOperation,
} from '../types/product'
import type { APIProvider, CommerceAPI } from '.'
import { GetAllCollectionsOperation } from '@commerce/types/collection';
const noop = () => {
throw new Error('Not implemented')
@@ -23,6 +25,9 @@ export const OPERATIONS = [
'getAllProductPaths',
'getAllProducts',
'getProduct',
'getAllFacets',
'getAllCollections',
] as const
export const defaultOperations = OPERATIONS.reduce((ops, k) => {
@@ -154,8 +159,43 @@ 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']>
}
getAllCollections: {
<T extends GetAllCollectionsOperation>(opts: {
variables?: T['variables']
config?: P['config']
preview?: boolean
}): Promise<T['data']>
<T extends GetAllCollectionsOperation>(
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,8 @@ Adding a commerce provider means adding a new folder in `framework` with a folde
- useSearch
- getProduct
- getAllProducts
- getAllFacets
- getAllCollections
- `wishlist`
- useWishlist
- useAddItem

View File

@@ -0,0 +1,54 @@
import { Asset } from '../../vendure/schema.d';
export type Collection = {
id: string
name: string
slug: string
description: string
featuredAsse: Asset
asset: Asset[]
}
export type SearchCollectionsBody = {
search?: string
sort?: string
locale?: string
}
export type CollectionTypes = {
collection: Collection
searchBody: SearchCollectionsBody
}
export type SearchCollectionsHook<T extends CollectionTypes = CollectionTypes> = {
data: {
collections: T['collection'][]
found: boolean
}
body: T['searchBody']
input: T['searchBody']
fetcherInput: T['searchBody']
}
export type CollectionsSchema<T extends CollectionTypes = CollectionTypes> = {
endpoint: {
options: {}
handlers: {
getCollections: SearchCollectionsHook<T>
}
}
}
export type GetAllCollectionsOperation<T extends CollectionTypes = CollectionTypes> = {
data: { collections: T['collection'][] }
variables: {
ids?: string[]
first?: number
}
}
export type GetCollectionOperation<T extends CollectionTypes = CollectionTypes> = {
data: { collection?: T['collection'] }
variables: { code: string; } | { code?: never; }
}

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,6 @@
import { CurrencyCode } from './../../vendure/schema.d';
import { FacetValueFilterInput, LogicalOperator, SearchResultSortParameter } from "@framework/schema"
export type ProductImage = {
url: string
alt?: string
@@ -40,11 +43,26 @@ export type Product = {
slug?: string
path?: string
images: ProductImage[]
variants: ProductVariant[]
price: ProductPrice
options: ProductOption[]
}
export type ProductCard = {
id: string
name: string
slug?: string
imageSrc: string
price: number
currencyCode: CurrencyCode
oldPrice?: number
discount?: number
weight?: number
facetValueIds?: string[],
collectionIds?: string[],
collection?: string,
isNotSell?: boolean
}
export type SearchProductsBody = {
search?: string
categoryId?: string | number
@@ -79,17 +97,24 @@ export type ProductsSchema<T extends ProductTypes = ProductTypes> = {
export type GetAllProductPathsOperation<
T extends ProductTypes = ProductTypes
> = {
data: { products: Pick<T['product'], 'path'>[] }
variables: { first?: number }
}
> = {
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,17 @@
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 getAllCollections from './operations/get-all-collection'
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 +42,8 @@ const operations = {
getAllProductPaths,
getAllProducts,
getProduct,
getAllFacets,
getAllCollections,
}
export const provider = { config, operations }

View File

@@ -0,0 +1,45 @@
import { OperationContext } from '@commerce/api/operations'
import { Collection } from '@commerce/types/collection'
import { Provider, VendureConfig } from '..'
import { GetAllCollectionsQuery } from '../../schema'
import { getAllCollectionsQuery } from '../../utils/queries/get-all-collections-query'
export type CollectionVariables = { first?: number }
export default function getAllCollectionsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllCollections(opts?: {
variables?: CollectionVariables
config?: Partial<VendureConfig>
preview?: boolean
}): Promise<{ collections: Collection[] }>
async function getAllCollections({
query = getAllCollectionsQuery,
variables: { ...vars } = {},
config: cfg,
}: {
query?: string
variables?: CollectionVariables
config?: Partial<VendureConfig>
preview?: boolean
} = {}): Promise<{ collections: Collection[] | any[] }> {
const config = commerce.getConfig(cfg)
const variables = {
input: {
take: vars.first,
groupByCollection: true,
},
}
const { data } = await config.fetch<GetAllCollectionsQuery>(query, {
variables,
})
return {
collections: data.collections.items,
}
}
return getAllCollections
}

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

@@ -1,3 +1,4 @@
import { FacetValue } from './schema.d';
export type Maybe<T> = T | null
export type Exact<T extends { [key: string]: unknown }> = {
[K in keyof T]: T[K]
@@ -93,6 +94,10 @@ export type QueryProductsArgs = {
options?: Maybe<ProductListOptions>
}
export type QueryFacetsArgs = {
options?: Maybe<FacetListOptions>
}
export type QuerySearchArgs = {
input: SearchInput
}
@@ -2730,6 +2735,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
@@ -2887,6 +2899,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>
@@ -3011,7 +3040,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' | 'facetValueIds' | 'collectionIds'
> & {
productAsset?: Maybe<
{ __typename?: 'SearchResultAsset' } & Pick<
@@ -3195,6 +3226,40 @@ 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 GetAllCollectionsQuery = { __typename?: 'Query' } & {
collections: { __typename?: 'CollectionList' } & {
items: Array<
{ __typename?: 'Collection' } & Pick<
Collection,
'id' | 'name' | 'slug'
> & {
parent?: Maybe<{ __typename?: 'Collection' } & Pick<Collection, 'id'>>
children?: Maybe<
Array<{ __typename?: 'Collection' } & Pick<Collection, 'id'>>
>
}
>,
'totalItems'
}
}
export type ActiveOrderQueryVariables = Exact<{ [key: string]: never }>
export type ActiveOrderQuery = { __typename?: 'Query' } & {

View File

@@ -19,6 +19,8 @@ export const searchResultFragment = /* GraphQL */ `
min
max
}
}
},
facetValueIds,
collectionIds,
}
`

View File

@@ -1,22 +1,23 @@
import { Product } from '@commerce/types/product'
import { Cart } from '@commerce/types/cart'
import { ProductCard } from '@commerce/types/product'
import { CartFragment, SearchResultFragment } from '../schema'
export function normalizeSearchResult(item: SearchResultFragment): Product {
export function normalizeSearchResult(item: SearchResultFragment): ProductCard {
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: [],
price: {
value: (item.priceWithTax as any).min / 100,
currencyCode: item.currencyCode,
},
options: [],
sku: item.sku,
imageSrc: item.productAsset?.preview ? item.productAsset?.preview + '?w=800&mode=crop' : '',
price: (item.priceWithTax as any).min / 100,
currencyCode: item.currencyCode,
facetValueIds: item.facetValueIds,
collectionIds: item.collectionIds,
// TODO:
// oldPrice: item.price
// discount
// isNotSell
// weight
}
}

View File

@@ -0,0 +1,12 @@
export const getAllCollectionsQuery = /* GraphQL */ `
query collections ($options: CollectionListOptions) {
collections (options: $options){
totalItems,
items {
id
name
slug
}
}
}
`

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,19 +1,37 @@
import { ProductCard } from '@commerce/types/product';
import { ProductVariables } from '@framework/api/operations/get-all-products';
import { Collection, FacetValue } 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 { FeaturedProductsCarousel, FreshProducts, HomeBanner, HomeCategories, HomeCollection, HomeCTA, HomeFeature, HomeRecipe, HomeSubscribe, HomeVideo } from 'src/components/modules/home';
import HomeSpice from 'src/components/modules/home/HomeSpice/HomeSpice';
import { CODE_FACET_DISCOUNT, CODE_FACET_FEATURED } from 'src/utils/constanst.utils';
import { getAllFacetValueIdsByParentCode, getAllFacetValuesForFeatuedProducts, getAllPromies, getFreshFacetId } from 'src/utils/funtion.utils';
import { PromiseWithKey } from 'src/utils/types.utils';
export default function Home() {
interface Props {
featuredAndDiscountFacetsValue: FacetValue[],
freshProducts: ProductCard[],
featuredProducts: ProductCard[],
collections: Collection[]
}
export default function Home({ featuredAndDiscountFacetsValue,
freshProducts, featuredProducts,
collections }: Props) {
return (
<>
<HomeBanner />
<HomeFeature />
<HomeCategories />
<FreshProducts data={freshProducts} collections={collections} />
<HomeCollection />
<HomeVideo />
<HomeSpice/>
<FeaturedProductsCarousel/>
<HomeSpice />
<FeaturedProductsCarousel data={featuredProducts} featuredFacetsValue={featuredAndDiscountFacetsValue} />
<HomeCTA />
<HomeRecipe />
<HomeRecipe />
<HomeSubscribe />
{/* // todo: uncomment
@@ -22,4 +40,76 @@ export default function Home() {
)
}
export async function getStaticProps({
preview,
locale,
locales,
}: GetStaticPropsContext) {
const config = { locale, locales }
let promisesWithKey = [] as PromiseWithKey[]
let props = {} as any
const { facets } = await commerce.getAllFacets({
variables: {},
config,
preview,
})
props.featuredAndDiscountFacetsValue = getAllFacetValuesForFeatuedProducts(facets)
// fresh products
const freshProductvariables: ProductVariables = {}
const freshFacetId = getFreshFacetId(facets)
if (freshFacetId) {
freshProductvariables.facetValueIds = [freshFacetId]
}
const freshProductsPromise = commerce.getAllProducts({
variables: freshProductvariables,
config,
preview,
})
promisesWithKey.push({ key: 'freshProducts', promise: freshProductsPromise, keyResult: 'products' })
// featured products
const allFeaturedFacetIds = getAllFacetValueIdsByParentCode(facets, CODE_FACET_FEATURED)
const allDiscountFacetIds = getAllFacetValueIdsByParentCode(facets, CODE_FACET_DISCOUNT)
const facetValueIdsForFeaturedProducts = [...allFeaturedFacetIds, ...allDiscountFacetIds]
const featuredProductsPromise = commerce.getAllProducts({
variables: {
facetValueIds: facetValueIdsForFeaturedProducts
},
config,
preview,
})
promisesWithKey.push({ key: 'featuredProducts', promise: featuredProductsPromise, keyResult: 'products' })
// collection
const collectionsPromise = commerce.getAllCollections({
variables: {},
config,
preview,
})
promisesWithKey.push({ key: 'collections', promise: collectionsPromise, keyResult: 'collections' })
try {
const promises = getAllPromies(promisesWithKey)
const rs = await Promise.all(promises)
promisesWithKey.map((item, index) => {
props[item.key] = item.keyResult ? rs[index][item.keyResult] : rs[index]
return null
})
return {
props,
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -21,6 +21,9 @@
width: 24rem;
height: 24rem;
}
img {
object-fit: contain;
}
}
.right{
margin-left: 1.2rem;

View File

@@ -1,39 +1,55 @@
import { ProductCard } from '@commerce/types/product'
import { Facet, FacetValue } from '@framework/schema'
import Link from 'next/link'
import React from 'react'
import { FeaturedProductProps } from 'src/utils/types.utils'
import s from './FeaturedProductCard.module.scss'
import { LANGUAGE } from '../../../utils/language.utils'
import ButtonIconBuy from '../ButtonIconBuy/ButtonIconBuy'
import ButtonCommon from '../ButtonCommon/ButtonCommon'
import { ROUTE } from 'src/utils/constanst.utils'
import { ImgWithLink } from '..'
export interface FeaturedProductCardProps extends FeaturedProductProps {
buttonText?: string
import { LANGUAGE } from '../../../utils/language.utils'
import ButtonCommon from '../ButtonCommon/ButtonCommon'
import ButtonIconBuy from '../ButtonIconBuy/ButtonIconBuy'
import s from './FeaturedProductCard.module.scss'
export interface FeaturedProductCardProps extends ProductCard {
buttonText?: string,
subText?: string,
}
const FeaturedProductCard = ({
imageSrc,
title,
subTitle,
name,
slug,
price,
originPrice,
subText,
currencyCode,
buttonText = LANGUAGE.BUTTON_LABEL.BUY_NOW,
}: FeaturedProductCardProps) => {
return (
<div className={s.featuredProductCardWarpper}>
<div className={s.left}>
<ImgWithLink src={imageSrc} alt={title}/>
<Link href={`${ROUTE.PRODUCT_DETAIL}/${slug}`}>
<a>
<ImgWithLink src={imageSrc} alt={name} />
</a>
</Link>
</div>
<div className={s.right}>
<div className={s.rightTop}>
<div className={s.title}>{title}</div>
<div className={s.subTitle}>{subTitle}</div>
<Link href={`${ROUTE.PRODUCT_DETAIL}/${slug}`}>
<a>
<div className={s.title}>{name}</div>
</a>
</Link>
<div className={s.subTitle}>{subText}</div>
<div className={s.priceWrapper}>
<div className={s.price}>{price} </div>
<div className={s.originPrice}>{originPrice} </div>
<div className={s.price}>{price} {currencyCode}</div>
{/* TODO: */}
{/* <div className={s.originPrice}>{originPrice} </div> */}
</div>
</div>
<div className={s.buttonWarpper}>
<div className={s.icon}>
<ButtonIconBuy size='default'/>
<ButtonIconBuy size='default' />
</div>
<div className={s.button}>
<ButtonCommon>{buttonText}</ButtonCommon>

View File

@@ -1,7 +1,7 @@
import React from 'react'
import s from './ImgWithLink.module.scss'
import Image from 'next/image'
import { BLUR_DATA_IMG } from 'src/utils/constanst.utils'
import { BLUR_DATA_IMG, DEFAULT_IMG } from 'src/utils/constanst.utils'
export interface ImgWithLinkProps {
src: string,
@@ -12,7 +12,7 @@ export interface ImgWithLinkProps {
const ImgWithLink = ({ src, alt, blurDataURL = BLUR_DATA_IMG }: ImgWithLinkProps) => {
return (
<div className={s.imgWithLink}>
<Image src={src} alt={alt}
<Image src={src || DEFAULT_IMG.src} alt={alt}
layout="fill"
className={s.imgWithLink}
placeholder="blur"

View File

@@ -2,6 +2,7 @@ import classNames from 'classnames'
import React from 'react'
import s from './LabelCommon.module.scss'
interface LabelCommonProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode
size?: 'default' | 'large'
shape?: 'half' | 'round' | 'default'
type?: 'default' | 'discount' | 'waiting' | 'delivering' | 'delivered'

View File

@@ -1,8 +1,8 @@
import { ProductCard } from '@commerce/types/product'
import Link from 'next/link'
import React from 'react'
import { IconBuy } from 'src/components/icons'
import { ROUTE } from 'src/utils/constanst.utils'
import { ProductProps } from 'src/utils/types.utils'
import { ImgWithLink } from '..'
import ButtonCommon from '../ButtonCommon/ButtonCommon'
import ButtonIconBuy from '../ButtonIconBuy/ButtonIconBuy'
@@ -11,16 +11,18 @@ import LabelCommon from '../LabelCommon/LabelCommon'
import s from './ProductCard.module.scss'
import ProductNotSell from './ProductNotSell/ProductNotSell'
export interface ProductCardProps extends ProductProps {
export interface ProductCardProps extends ProductCard {
buttonText?: string
isSingleButton?: boolean,
}
const ProductCard = ({
category,
const ProductCardComponent = ({
collection,
name,
slug,
weight,
price,
currencyCode,
buttonText = 'Buy Now',
imageSrc,
isNotSell,
@@ -35,24 +37,31 @@ const ProductCard = ({
return (
<div className={s.productCardWarpper}>
<div className={s.cardTop}>
<Link href={`${ROUTE.PRODUCT_DETAIL}/test`}>
<div className={s.productImage}>
<ImgWithLink src={imageSrc} alt={name}/>
</div>
<Link href={`${ROUTE.PRODUCT_DETAIL}/${slug}`}>
<a>
<div className={s.productImage}>
<ImgWithLink src={imageSrc} alt={name}/>
</div>
</a>
</Link>
{
collection &&
<div className={s.productLabel}>
<LabelCommon shape="half">{category}</LabelCommon>
<LabelCommon shape="half">{collection}</LabelCommon>
</div>
}
</div>
<div className={s.cardMid}>
<div className={s.cardMidTop}>
<Link href={`${ROUTE.PRODUCT_DETAIL}/test`}>
<div className={s.productname}>{name} </div>
<Link href={`${ROUTE.PRODUCT_DETAIL}/${slug}`}>
<a>
<div className={s.productname}>{name} </div>
</a>
</Link>
<div className={s.productWeight}>{weight}</div>
</div>
<div className={s.cardMidBot}>
<div className={s.productPrice}>{price}</div>
<div className={s.productPrice}>{price} {currencyCode}</div>
<div className={s.wishList}>
<ItemWishList />
</div>
@@ -80,4 +89,4 @@ const ProductCard = ({
)
}
export default ProductCard
export default ProductCardComponent

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

@@ -12,7 +12,7 @@ interface ColectionCarcouselProps extends CollectionHeadingProps {
data: ProductCardProps[]
itemKey: string
viewAllLink?: string,
category:string
category?: string
}
const ColectionCarcousel = ({
@@ -21,7 +21,8 @@ const ColectionCarcousel = ({
title,
subtitle,
type,
category
category,
viewAllLink = ROUTE.PRODUCTS,
}: ColectionCarcouselProps) => {
return (
<div className={s.colectionCarcoucelWarpper}>
@@ -34,7 +35,7 @@ const ColectionCarcousel = ({
></CollectionHeading>
</div>
<div className={s.right}>
<ViewAllItem link={`${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=${category}`}/>
<ViewAllItem link={category ? `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=${category}` : viewAllLink} />
</div>
</div>
<div className={s.bot}>

View File

@@ -1,92 +1,89 @@
import React from 'react'
import { FacetValue } from '@framework/schema'
import React, { useMemo } from 'react'
import { ResponsiveType } from 'react-multi-carousel'
import { CarouselCommon, FeaturedProductCard,HeadingCommon} from 'src/components/common'
import { CarouselCommon, FeaturedProductCard, HeadingCommon } from 'src/components/common'
import { FeaturedProductCardProps } from 'src/components/common/FeaturedProductCard/FeaturedProductCard'
import { getFacetNamesFromIds } from 'src/utils/funtion.utils'
import s from "./FeaturedProductsCarousel.module.scss"
interface FeaturedProductsCarouselProps {
title?: string
data: FeaturedProductCardProps[]
featuredFacetsValue: FacetValue[],
}
const dataDemo:FeaturedProductCardProps[] = [{
title: "Sale 25% Coffee Bean",
subTitle: "50 first Orders within a day",
originPrice: "$20.00",
price: "$14.00",
imageSrc: "https://user-images.githubusercontent.com/76099413/133043628-db7813f9-1bb7-4ee1-b028-dc4295563494.png"
},{
title: "Sale 20% Fruits",
subTitle: "50 first Orders within a day",
originPrice: "$20.00",
price: "$14.00",
imageSrc: "https://user-images.githubusercontent.com/76099413/133043630-07a353b9-573d-4c1d-b1de-2c932e3f14f7.png"
},{
title: "Sale 25% Coffee Bean",
subTitle: "50 first Orders within a day",
originPrice: "$20.00",
price: "$14.00",
imageSrc: "https://user-images.githubusercontent.com/76099413/133043633-954c105b-c703-4e5c-8f5f-7943ad633ff0.png"
}]
const RESPONSIVE: ResponsiveType = {
hugeScreen: {
breakpoint: { max: 9999, min: 1500 },
items: 2.25,
slidesToSlide: 1, // optional, default to 1.
},
largeScreen: {
breakpoint: { max: 1500, min: 1440 },
items: 2.075,
slidesToSlide: 1, // optional, default to 1.
},
largeDesktop: {
breakpoint: { max: 1440, min: 1280 },
items: 1.75,
slidesToSlide: 1, // optional, default to 1.
},
desktop: {
breakpoint: { max: 1280, min: 1148 },
items: 1.5,
slidesToSlide: 1, // optional, default to 1.
},
smallDesktop: {
breakpoint: { max: 1148, min: 1024 },
items: 1.375,
slidesToSlide: 1, // optional, default to 1.
},
lap: {
breakpoint: { max: 1024, min: 968 },
items: 1.7,
},
tablet: {
breakpoint: { max: 968, min: 768 },
items: 1.075,
},
smallTablet: {
breakpoint: { max: 768, min: 640 },
items: 1.25,
},
largeMobile: {
breakpoint: { max: 640, min: 400 },
items: 1.275,
},
mobile: {
breakpoint: { max: 400, min: 300 },
items: 1.1,
},
smallMobile: {
breakpoint: { max: 300, min: 0 },
items: 1,
},
}
const RESPONSIVE: ResponsiveType = {
hugeScreen: {
breakpoint: { max: 9999, min: 1500 },
items: 2.25,
slidesToSlide: 1, // optional, default to 1.
},
largeScreen: {
breakpoint: { max: 1500, min: 1440 },
items: 2.075,
slidesToSlide: 1, // optional, default to 1.
},
largeDesktop: {
breakpoint: { max: 1440, min: 1280 },
items: 1.75,
slidesToSlide: 1, // optional, default to 1.
},
desktop: {
breakpoint: { max: 1280, min: 1148 },
items: 1.5,
slidesToSlide: 1, // optional, default to 1.
},
smallDesktop: {
breakpoint: { max: 1148, min: 1024 },
items: 1.375,
slidesToSlide: 1, // optional, default to 1.
},
lap: {
breakpoint: { max: 1024, min: 968 },
items: 1.7,
},
tablet: {
breakpoint: { max: 968, min: 768 },
items: 1.075,
},
smallTablet: {
breakpoint: { max: 768, min: 640 },
items: 1.25,
},
largeMobile: {
breakpoint: { max: 640, min: 400 },
items: 1.275,
},
mobile: {
breakpoint: { max: 400, min: 300 },
items: 1.1,
},
smallMobile: {
breakpoint: { max: 300, min: 0 },
items: 1,
},
}
const FeaturedProductsCarousel = ({title="Featured Products"}: FeaturedProductsCarouselProps) => {
return (
<div className={s.warpper}>
<div className={s.heading}>
<HeadingCommon>{title}</HeadingCommon>
</div>
<CarouselCommon<FeaturedProductCardProps> data={dataDemo} Component={FeaturedProductCard} itemKey="featured-products" responsive={RESPONSIVE}/>
</div>
)
const FeaturedProductsCarousel = ({ data, featuredFacetsValue }: FeaturedProductsCarouselProps) => {
const featuredProducts = useMemo(() => {
return data.map(item => {
return {
...item,
subText: getFacetNamesFromIds(featuredFacetsValue, item.facetValueIds)
}
})
}, [data, featuredFacetsValue])
return (
<div className={s.warpper}>
<div className={s.heading}>
<HeadingCommon>Featured Products</HeadingCommon>
</div>
<CarouselCommon<FeaturedProductCardProps>
data={featuredProducts}
defaultComponentProps={featuredFacetsValue}
Component={FeaturedProductCard}
itemKey="featured-products"
responsive={RESPONSIVE} />
</div>
)
}
export default FeaturedProductsCarousel

View File

@@ -0,0 +1,47 @@
import { ProductCard } from '@commerce/types/product'
import { Collection } from '@framework/schema'
import React, { useMemo } from 'react'
import { OPTION_ALL, QUERY_KEY, ROUTE } from 'src/utils/constanst.utils'
import { CollectionCarcousel } from '..'
interface FreshProductsProps {
data: ProductCard[]
collections: Collection[]
}
const getCategoryNameFromCollectionId = (colelctions: Collection[], collectionId?: string ) => {
if (!collectionId) {
return ''
}
const collection = colelctions.find(item => item.id === collectionId)
return collection?.name || ''
}
const FreshProducts = ({ data, collections }: FreshProductsProps) => {
const dataWithCategory = useMemo(() => {
return data.map(item => {
return {
...item,
collection: getCategoryNameFromCollectionId(collections, item.collectionIds ? item.collectionIds[0] : undefined)
}
})
}, [data, collections])
if (data.length === 0) {
return null
}
return (
<div className="w-full">
<CollectionCarcousel
type="highlight"
data={dataWithCategory}
itemKey="product-1"
title="Fresh Products Today"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
viewAllLink={`${ROUTE.PRODUCTS}/?${QUERY_KEY.FEATURED}=${OPTION_ALL}`}
/>
</div>
)
}
export default FreshProducts

View File

@@ -95,14 +95,6 @@ const dataTest = [
const HomeCollection = (props: HomeCollectionProps) => {
return (
<div className="w-full">
<CollectionCarcousel
type="highlight"
data={dataTest}
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"

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

@@ -1,41 +1,44 @@
import DefaultImg from '../../public/assets/images/default_img.jpg'
export const BLUR_DATA_IMG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mN8fBIAApUBruKYvzsAAAAASUVORK5CYII='
export const DEFAULT_IMG = DefaultImg
export const SOCIAL_LINKS = {
FB: 'FB',
TWITTER: 'TWITTER',
YOUTUBE: 'YOUTUBE',
IG: 'IG',
FB: 'FB',
TWITTER: 'TWITTER',
YOUTUBE: 'YOUTUBE',
IG: 'IG',
}
export const ROUTE = {
HOME: '/',
ABOUT: '/about',
ACCOUNT: '/account',
HOME: '/',
ABOUT: '/about',
ACCOUNT: '/account',
PRODUCTS: '/products',
PRODUCT_DETAIL: '/product',
BLOGS: '/blogs',
BLOG_DETAIL: '/blog',
PRODUCTS: '/products',
PRODUCT_DETAIL: '/product',
RECIPES: '/recipes',
RECIPE_DETAIL: '/recipe',
BLOGS: '/blogs',
BLOG_DETAIL: '/blog',
NOTIFICATION: '/notifications',
BUSSINESS: '/bussiness',
CONTACT: '/contact',
CHECKOUT: '/checkout',
FAQ: '/faq',
CUSTOMER_SERVICE: '/customer-service',
TERM_CONDITION: '/term-condition',
PRIVACY_POLICY: '/privacy-policy',
FORGOT_PASSWORD: '/forgot-password'
RECIPES: '/recipes',
RECIPE_DETAIL: '/recipe',
NOTIFICATION: '/notifications',
BUSSINESS: '/bussiness',
CONTACT: '/contact',
CHECKOUT: '/checkout',
FAQ: '/faq',
CUSTOMER_SERVICE: '/customer-service',
TERM_CONDITION: '/term-condition',
PRIVACY_POLICY: '/privacy-policy',
FORGOT_PASSWORD: '/forgot-password'
}
export const ACCOUNT_TAB = {
CUSTOMER_INFO: '',
ORDER: 'orders',
FAVOURITE: 'wishlist',
CUSTOMER_INFO: '',
ORDER: 'orders',
FAVOURITE: 'wishlist',
}
export const LOCAL_STORAGE_KEY = {
@@ -43,93 +46,99 @@ export const LOCAL_STORAGE_KEY = {
}
export const QUERY_KEY = {
TAB: 'tab',
CATEGORY: 'category',
BRAND: 'brand',
FEATURED: 'feature',
SORTBY:'sortby',
RECIPES:'recipes'
TAB: 'tab',
CATEGORY: 'category',
BRAND: 'brand',
FEATURED: 'feature',
SORTBY: 'sortby',
RECIPES: 'recipes'
}
export enum ProductFeature {
BestSellers = 'Best Sellers',
Sales = 'Sales',
NewItem = 'New Item',
Viewed = 'Viewed',
BestSellers = 'Best Sellers',
Sales = 'Sales',
NewItem = 'New Item',
Viewed = 'Viewed',
}
export const KEY = {
ENTER: 'Enter',
ENTER: 'Enter',
}
export const OPTION_ALL = 'all';
export const DEFAULT_PAGE_SIZE=20;
export const DEFAULT_PAGE_SIZE = 20;
export const CATEGORY = [
{
name: 'All',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=${OPTION_ALL}`,
},
{
name: 'Veggie',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=veggie`,
},
{
name: 'Seafood',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=seafood`,
},
{
name: 'Frozen',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=frozen`,
},
{
name: 'Coffee Bean',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=coffee_bean`,
},
{
name: 'Sauce',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=sauce`,
},
]
export const BRAND = [
{
name: 'Maggi',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=maggi`,
},
{
name: 'Chomilex',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=chomilex`,
},
{
name: 'Chinsu',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=chinsu`,
},
]
export const FEATURED = [
{
name: 'Best Sellers',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.FEATURED}=best_sellers`,
},
{
name: 'Sales',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.FEATURED}=sales`,
},
{
name: 'New Item',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.FEATURED}=new_item`,
},
{
name: 'Viewed',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.FEATURED}=viewed`,
},
]
export const DEFAULT_BLOG_PAGE_SIZE=6;
{
name: 'All',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=${OPTION_ALL}`,
},
{
name: 'Veggie',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=veggie`,
},
{
name: 'Seafood',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=seafood`,
},
{
name: 'Frozen',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=frozen`,
},
{
name: 'Coffee Bean',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=coffee_bean`,
},
{
name: 'Sauce',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.CATEGORY}=sauce`,
},
]
export const FILTER_PAGE = [ROUTE.HOME,ROUTE.PRODUCTS]
export const BRAND = [
{
name: 'Maggi',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=maggi`,
},
{
name: 'Chomilex',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=chomilex`,
},
{
name: 'Chinsu',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=chinsu`,
},
]
export const CODE_FACET_FEATURED = 'featured'
export const CODE_FACET_DISCOUNT = 'discount'
export const CODE_FACET_FEATURED_VARIANT = {
FRESH: 'fresh',
}
export const FEATURED = [
{
name: 'Best Sellers',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.FEATURED}=best_sellers`,
},
{
name: 'Sales',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.FEATURED}=sales`,
},
{
name: 'New Item',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.FEATURED}=new_item`,
},
{
name: 'Viewed',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.FEATURED}=viewed`,
},
]
export const DEFAULT_BLOG_PAGE_SIZE = 6;
export const FILTER_PAGE = [ROUTE.HOME, ROUTE.PRODUCTS]
export const STATE_OPTIONS = [
{
@@ -141,3 +150,4 @@ export const STATE_OPTIONS = [
value: 'Hà Nội',
},
]

View File

@@ -1,11 +1,63 @@
import { Facet } from "@commerce/types/facet";
import { FacetValue } from './../../framework/vendure/schema.d';
import { CODE_FACET_DISCOUNT, CODE_FACET_FEATURED, CODE_FACET_FEATURED_VARIANT } from "./constanst.utils";
import { PromiseWithKey } from "./types.utils";
export function isMobile() {
return window.innerWidth < 768
return window.innerWidth < 768
}
export function removeItem<T>(arr: Array<T>, value: T): Array<T> {
const index = arr.indexOf(value);
if (index > -1) {
arr.splice(index, 1);
}
return [...arr];
}
export function removeItem<T>(arr: Array<T>, value: T): Array<T> {
const index = arr.indexOf(value);
if (index > -1) {
arr.splice(index, 1);
}
return [...arr];
}
function findFacetByCode(code: string, facets?: Facet) {
return facets?.values.find((item: FacetValue) => item.code === code)
}
export function getFeaturedFacetId(facets: Facet[]) {
const featuredFacet = facets.find((item: Facet) => item.code === CODE_FACET_FEATURED)
return featuredFacet?.id
}
export function getFreshFacetId(facets: Facet[]) {
const featuredFacet = facets.find((item: Facet) => item.code === CODE_FACET_FEATURED)
const freshFacetValue = findFacetByCode(CODE_FACET_FEATURED_VARIANT.FRESH, featuredFacet)
return freshFacetValue?.id
}
export function getAllFacetValueIdsByParentCode(facets: Facet[], code: string) {
const featuredFacet = facets.find((item: Facet) => item.code === code)
const rs = featuredFacet?.values.map((item: FacetValue) => item.id)
return rs || []
}
export function getAllFacetValuesForFeatuedProducts(facets: Facet[]) {
const facetsRs = facets.filter((item: Facet) => item.code === CODE_FACET_FEATURED || item.code === CODE_FACET_DISCOUNT)
let rs = [] as FacetValue[]
facetsRs.map((item: Facet) => {
rs = rs.concat(item.values)
return null
})
return rs
}
export function getFacetNamesFromIds(facets: FacetValue[], ids?: string[]): string {
if (!ids || ids?.length === 0) {
return ''
}
const facetItems = facets.filter((item: FacetValue) => ids.includes(item.id))
const names = facetItems.map((item: FacetValue) => item.name)
return names.join(", ")
}
export function getAllPromies (promies: PromiseWithKey[]) {
return promies.map(item => item.promise)
}

View File

@@ -50,4 +50,10 @@ export type filterContextType = {
visible: boolean;
open: () => void;
close: () => void;
};
};
export type PromiseWithKey = {
key: string
promise: PromiseLike<any>
keyResult?: string,
}