🔀 merge: branch 'release-stable' of https://github.com/KieIO/grocery-vercel-commerce into checkout

:%s
This commit is contained in:
lytrankieio123
2021-10-18 16:23:22 +07:00
38 changed files with 768 additions and 146 deletions

View File

@@ -17,6 +17,7 @@ export type LineItem = {
quantity: number
discounts: Discount[]
// A human-friendly unique string automatically generated from the products name
slug: string
path: string
variant: ProductVariant
options?: SelectedOption[]

View File

@@ -30,6 +30,7 @@ export type ProductOptionValues = {
export type ProductVariant = {
id: string | number
name:string,
options: ProductOption[]
availableForSale?: boolean
}
@@ -48,7 +49,8 @@ export type Product = {
options: ProductOption[]
facetValueIds?: string[]
collectionIds?: string[]
collection?: string,
collection?: string[],
variants?: ProductVariant[]
}
export type ProductCard = {
@@ -65,6 +67,8 @@ export type ProductCard = {
collectionIds?: string[],
collection?: string,
isNotSell?: boolean
productVariantId?:string
productVariantName?:string
}
export type SearchProductsBody = {

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, facetValueIds?: string[],collectionSlug?:string }
export type ProductVariables = { first?: number, facetValueIds?: string[], collectionSlug?:string, groupByProduct?:boolean }
export default function getAllProductsOperation({
commerce,
@@ -31,8 +31,8 @@ export default function getAllProductsOperation({
input: {
take: vars.first,
facetValueIds: vars.facetValueIds,
collectionSlug: vars.collectionSlug,
groupByProduct: true,
collectionSlug : vars.collectionSlug,
groupByProduct: vars.groupByProduct??true,
},
}
const { data } = await config.fetch<GetAllProductsQuery>(query, {

View File

@@ -36,6 +36,7 @@ export default function getProductOperation({
})),
variants: product.variants.map((v) => ({
id: v.id,
name:v.name,
options: v.options.map((o) => ({
// This __typename property is required in order for the correct
// variant selection to work, see `components/product/helpers.ts`
@@ -54,7 +55,8 @@ export default function getProductOperation({
values: og.options.map((o) => ({ label: o.name })),
})),
facetValueIds: product.facetValues.map(item=> item.id),
collectionIds: product.collections.map(item => item.id)
collectionIds: product.collections.map(item => item.id),
collection:product.collections.map(item => item.name),
} as Product
}

View File

@@ -3055,7 +3055,7 @@ export type SearchResultFragment = { __typename?: 'SearchResult' } & Pick<
SearchResult,
'productId' | 'sku' | 'productName' | 'description' | 'slug' | 'sku' | 'currencyCode'
| 'productAsset' | 'price' | 'priceWithTax' | 'currencyCode'
| 'collectionIds' | 'facetValueIds' | 'collectionIds'
| 'collectionIds' | 'productVariantId' | 'facetValueIds' | "productVariantName"
> & {
productAsset?: Maybe<
{ __typename?: 'SearchResultAsset' } & Pick<
@@ -3381,7 +3381,7 @@ export type GetProductQuery = { __typename?: 'Query' } & {
variants: Array<
{ __typename?: 'ProductVariant' } & Pick<
ProductVariant,
'id' | 'priceWithTax' | 'currencyCode' | 'price'
'id' | 'priceWithTax' | 'currencyCode' | 'price' | "name"
> & {
options: Array<
{ __typename?: 'ProductOption' } & Pick<
@@ -3425,7 +3425,7 @@ export type GetProductQuery = { __typename?: 'Query' } & {
collections: Array<
{ __typename?: 'Collection' } & Pick<
Collection,
'id'
'id'|"name"
>
>
}

View File

@@ -7,6 +7,8 @@ export const searchResultFragment = /* GraphQL */ `
slug
sku
currencyCode
productVariantId
productVariantName
productAsset {
id
preview

View File

@@ -1,6 +1,6 @@
import { Cart } from '@commerce/types/cart'
import { ProductCard } from '@commerce/types/product'
import { CartFragment, SearchResultFragment,Favorite,ActiveCustomerQuery } from '../schema'
import { ProductCard, Product } from '@commerce/types/product'
import { CartFragment, SearchResultFragment,Favorite } from '../schema'
export function normalizeSearchResult(item: SearchResultFragment): ProductCard {
return {
@@ -10,6 +10,8 @@ export function normalizeSearchResult(item: SearchResultFragment): ProductCard {
imageSrc: item.productAsset?.preview ? item.productAsset?.preview + '?w=800&mode=crop' : '',
price: (item.priceWithTax as any).min / 100,
currencyCode: item.currencyCode,
productVariantId: item.productVariantId,
productVariantName:item.productVariantName,
facetValueIds: item.facetValueIds,
collectionIds: item.collectionIds,
@@ -47,7 +49,7 @@ export function normalizeCart(order: CartFragment): Cart {
id: l.id,
name: l.productVariant.name,
quantity: l.quantity,
url: l.productVariant.product.slug,
slug: l.productVariant.product.slug,
variantId: l.productVariant.id,
productId: l.productVariant.productId,
images: [{ url: l.featuredAsset?.preview + '?preset=thumb' || '' }],
@@ -67,3 +69,18 @@ export function normalizeCart(order: CartFragment): Cart {
})),
}
}
export function normalizeProductCard(product: Product): ProductCard {
return {
id: product.id,
name: product.name,
slug: product.slug,
imageSrc: product.images[0].url,
price: product.price,
currencyCode: product.currencyCode,
productVariantId: product.variants?.[0].id.toString(),
productVariantName:product.variants?.[0].name,
facetValueIds: product.facetValueIds,
collectionIds: product.collectionIds,
}
}

View File

@@ -12,6 +12,7 @@ export const getProductQuery = /* GraphQL */ `
}
variants {
id
name
priceWithTax
currencyCode
options {
@@ -41,6 +42,7 @@ export const getProductQuery = /* GraphQL */ `
}
collections {
id
name
}
}
}

3
next-env.d.ts vendored
View File

@@ -1,3 +1,6 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@@ -6,6 +6,8 @@ import { GetStaticPropsContext } from 'next';
import { Layout } from 'src/components/common';
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 { FACET } from 'src/utils/constanst.utils';
import { FilterOneVatiant, getFacetIdByName } from 'src/utils/funtion.utils';
import { CODE_FACET_DISCOUNT, CODE_FACET_FEATURED,COLLECTION_SLUG_SPICE } from 'src/utils/constanst.utils';
import { getAllFacetValueIdsByParentCode, getAllFacetValuesForFeatuedProducts, getAllPromies, getFreshFacetId } from 'src/utils/funtion.utils';
import { PromiseWithKey } from 'src/utils/types.utils';
@@ -16,8 +18,10 @@ interface Props {
featuredProducts: ProductCard[],
collections: Collection[]
spiceProducts:ProductCard[]
veggie: ProductCard[],
}
export default function Home({ featuredAndDiscountFacetsValue,
export default function Home({ featuredAndDiscountFacetsValue, veggie,
freshProducts, featuredProducts,
collections,spiceProducts }: Props) {
@@ -26,8 +30,8 @@ export default function Home({ featuredAndDiscountFacetsValue,
<HomeBanner />
<HomeFeature />
<HomeCategories />
<HomeCollection data = {veggie}/>
<FreshProducts data={freshProducts} collections={collections} />
<HomeCollection />
<HomeVideo />
{spiceProducts.length>0 && <HomeSpice data={spiceProducts}/>}
<FeaturedProductsCarousel data={featuredProducts} featuredFacetsValue={featuredAndDiscountFacetsValue} />
@@ -56,6 +60,8 @@ export async function getStaticProps({
config,
preview,
})
props.featuredAndDiscountFacetsValue = getAllFacetValuesForFeatuedProducts(facets)
// fresh products
@@ -73,7 +79,20 @@ export async function getStaticProps({
props.freshProducts = []
}
//veggie
const veggieProductvariables: ProductVariables = {
groupByProduct:false
}
const veggieId = getFacetIdByName(facets,FACET.CATEGORY.PARENT_NAME,FACET.CATEGORY.VEGGIE)
if (veggieId) {
veggieProductvariables.facetValueIds = [veggieId]
}
const veggieProductsPromise = commerce.getAllProducts({
variables: veggieProductvariables,
config,
preview,
})
promisesWithKey.push({ key: 'veggie', promise: veggieProductsPromise, keyResult: 'products' })
// featured products
const allFeaturedFacetIds = getAllFacetValueIdsByParentCode(facets, CODE_FACET_FEATURED)
const allDiscountFacetIds = getAllFacetValueIdsByParentCode(facets, CODE_FACET_DISCOUNT)
@@ -115,7 +134,7 @@ export async function getStaticProps({
const rs = await Promise.all(promises)
promisesWithKey.map((item, index) => {
props[item.key] = item.keyResult ? rs[index][item.keyResult] : rs[index]
props[item.key] = item.keyResult ? FilterOneVatiant(rs[index][item.keyResult]) : rs[index]
return null
})
return {

View File

@@ -1,22 +1,43 @@
import { Product } from '@framework/schema'
import { Collection } from '@commerce/types/collection'
import { Product, ProductCard } from '@commerce/types/product'
import commerce from '@lib/api/commerce'
import { GetStaticPathsContext, GetStaticPropsContext, InferGetStaticPropsType } from 'next'
import { useEffect, useState } from 'react'
import { Layout, RecipeDetail, RecommendedRecipes, RelevantBlogPosts } from 'src/components/common'
import { useLocalStorage } from 'src/components/hooks/useLocalStorage'
import { ProductInfoDetail, ReleventProducts, ViewedProducts } from 'src/components/modules/product-detail'
import { MAX_PRODUCT_CAROUSEL, REVALIDATE_TIME } from 'src/utils/constanst.utils'
import { LOCAL_STORAGE_KEY, MAX_PRODUCT_CAROUSEL, REVALIDATE_TIME } from 'src/utils/constanst.utils'
import { BLOGS_DATA_TEST, INGREDIENT_DATA_TEST, RECIPE_DATA_TEST } from 'src/utils/demo-data'
import { getAllPromies } from 'src/utils/funtion.utils'
import { normalizeProductCard } from '@framework/utils/normalize';
import { PromiseWithKey } from 'src/utils/types.utils'
export default function Slug({ product, relevantProducts, collections }: InferGetStaticPropsType<typeof getStaticProps>) {
interface Props {
relevantProducts: ProductCard[],
product: Product,
collections: Collection[]
}
export default function Slug({ product, relevantProducts, collections }: Props) {
const [local,setLocal] = useLocalStorage<Product[]>(LOCAL_STORAGE_KEY.VIEWEDPRODUCT, []);
const [viewed, setViewed] = useState<ProductCard[]>([])
useEffect(() => {
if(local){
if(!local.find(p => p.id === product.id)){
setLocal([...local, product])
}else{
setViewed(local.filter((p)=>p.id !== product.id).map((p)=>normalizeProductCard(p)))
}
}else{
setLocal([product])
}
}, [product])
return <>
<ProductInfoDetail productDetail={product} collections={collections}/>
<ProductInfoDetail productDetail={product}/>
<RecipeDetail ingredients={INGREDIENT_DATA_TEST} />
<RecommendedRecipes data={RECIPE_DATA_TEST} />
<ReleventProducts data={relevantProducts} collections={collections}/>
<ViewedProducts />
<ViewedProducts data={viewed}/>
<RelevantBlogPosts data={BLOGS_DATA_TEST} title="relevent blog posts" />
</>
}
@@ -68,7 +89,6 @@ export async function getStaticProps({
})
promisesWithKey.push({ key: 'collections', promise: collectionsPromise, keyResult: 'collections' })
try {
const promises = getAllPromies(promisesWithKey)
const rs = await Promise.all(promises)

View File

@@ -1,5 +1,7 @@
import { normalizeCart } from '@framework/utils/normalize';
import React from 'react';
import { useCartDrawer } from 'src/components/contexts';
import useGetActiveOrder from 'src/components/hooks/cart/useGetActiveOrder';
import { PRODUCT_CART_DATA_TEST } from 'src/utils/demo-data';
import { DrawerCommon } from '..';
import s from './CartDrawer.module.scss';
@@ -14,14 +16,15 @@ interface Props {
const CartDrawer = ({ }: Props) => {
const { cartVisible, closeCartDrawer } = useCartDrawer()
const {order} = useGetActiveOrder()
return (
<DrawerCommon
title={`Your cart (${PRODUCT_CART_DATA_TEST.length})`}
title={`Your cart (${order?.lineItems.length})`}
visible={cartVisible}
onClose={closeCartDrawer}>
<div className={s.cartDrawer}>
<div className={s.body}>
<ProductsInCart data={PRODUCT_CART_DATA_TEST} />
<ProductsInCart data={order?.lineItems||[]} currency={order?.currency||{code:"USA"}}/>
<CartRecommendation />
</div>
<div>

View File

@@ -1,25 +1,64 @@
import React from 'react';
import React, { useCallback, useState } from 'react'
import Link from 'next/link'
import { QuanittyInput } from 'src/components/common';
import { IconDelete } from 'src/components/icons';
import { ROUTE } from 'src/utils/constanst.utils';
import { ProductProps } from 'src/utils/types.utils';
import ImgWithLink from '../../../ImgWithLink/ImgWithLink';
import LabelCommon from '../../../LabelCommon/LabelCommon';
import s from './ProductCartItem.module.scss';
import { ModalConfirm, QuanittyInput } from 'src/components/common'
import { IconDelete } from 'src/components/icons'
import { ROUTE } from 'src/utils/constanst.utils'
import ImgWithLink from '../../../ImgWithLink/ImgWithLink'
import LabelCommon from '../../../LabelCommon/LabelCommon'
import s from './ProductCartItem.module.scss'
import { LineItem } from '@commerce/types/cart'
import { useUpdateProductInCart } from 'src/components/hooks/cart'
import { debounce } from 'lodash'
import useRemoveProductInCart from 'src/components/hooks/cart/useRemoveProductInCart'
export interface ProductCartItempProps extends ProductProps {
quantity: number,
export interface ProductCartItempProps extends LineItem {
currency: { code: string }
}
const ProductCartItem = ({ name, slug, weight, price, oldPrice, discount, imageSrc, quantity }: ProductCartItempProps) => {
const ProductCartItem = ({
slug,
discounts,
quantity,
variant,
name,
currency,
id
}: ProductCartItempProps) => {
const [visible, setVisible] = useState(false)
const {updateProduct} = useUpdateProductInCart()
const {removeProduct, loading} = useRemoveProductInCart()
const handleQuantityChangeCallback = (isSuccess:boolean,mess?:string) => {
if(!isSuccess){
console.log(mess)
}
}
const handleRemoveCallback = (isSuccess:boolean,mess?:string) => {
if(!isSuccess){
console.log(mess)
}else{
setVisible(false)
}
}
const handleQuantityChange = (value:number) => {
updateProduct({orderLineId:id,quantity:value},handleQuantityChangeCallback)
}
const debounceFn = useCallback(debounce(handleQuantityChange, 500), []);
const handleCancel = () => {
setVisible(false)
}
const handleOpen = () => {
setVisible(true)
}
const handleConfirm = () => {
removeProduct({orderLineId:id},handleRemoveCallback)
}
return (
<div className={s.productCartItem}>
<div className={s.info}>
<Link href={`${ROUTE.PRODUCT_DETAIL}/${slug}`}>
<a href="">
<div className={s.imgWrap}>
<ImgWithLink src={imageSrc} alt={name} />
<ImgWithLink src={variant?.image?.url ?? ''} alt={name} />
</div>
</a>
</Link>
@@ -27,30 +66,32 @@ const ProductCartItem = ({ name, slug, weight, price, oldPrice, discount, imageS
<Link href={`${ROUTE.PRODUCT_DETAIL}/${slug}`}>
<a>
<div className={s.name}>
{name} {weight ? `(${weight})` : ''}
{name} {variant?.weight ? `(${variant.weight})` : ''}
</div>
</a>
</Link>
<div className={s.price}>
{
oldPrice &&
<div className={s.old}>
<span className={s.number}>{oldPrice}</span>
<LabelCommon type='discount'>{discount}</LabelCommon>
</div>
}
<div className={s.current}>{price}</div>
{discounts.length > 0 && (
<div className={s.old}>
{/* <span className={s.number}>{oldPrice}</span> */}
<LabelCommon type="discount">{discounts[0]}</LabelCommon>
</div>
)}
<div className={s.current}>{variant?.price} {currency?.code}</div>
</div>
</div>
</div>
<div className={s.actions}>
<div className={s.iconDelete}>
<div className={s.iconDelete} onClick={handleOpen}>
<IconDelete />
</div>
<QuanittyInput size='small' initValue={quantity} />
<QuanittyInput size="small" initValue={quantity} onChange={debounceFn}/>
</div>
<ModalConfirm visible={visible} onClose={handleCancel} onCancel={handleCancel} onOk={handleConfirm} loading={loading}>
Are you sure want to remove {name} form your cart
</ModalConfirm>
</div>
)
}
export default ProductCartItem;
export default ProductCartItem

View File

@@ -1,25 +1,21 @@
import { LineItem } from '@commerce/types/cart';
import React from 'react';
import ProductCartItem, { ProductCartItempProps } from '../ProductCartItem/ProductCartItem';
import s from './ProductsInCart.module.scss';
interface Props {
data: ProductCartItempProps[]
data: LineItem[]
currency: { code: string }
}
const ProductsInCart = ({ data }: Props) => {
const ProductsInCart = ({ data, currency }: Props) => {
return (
<ul className={s.productsInCart}>
{
data.map(item => <li key={item.name}>
<ProductCartItem
name={item.name}
slug={item.slug}
weight={item.weight}
price={item.price}
oldPrice={item.oldPrice}
discount={item.discount}
imageSrc={item.imageSrc}
quantity={item.quantity}
currency = {currency}
{...item}
/>
</li>)
}

View File

@@ -1,5 +1,4 @@
import classNames from 'classnames';
import { route } from 'next/dist/server/router';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import s from './MenuFilterItem.module.scss';

View File

@@ -5,6 +5,7 @@ import s from './ModalConfirm.module.scss'
interface ModalConfirmProps extends ModalCommonProps {
okText?: String
cancelText?: String
loading?:boolean
onOk?: () => void
onCancel?: () => void
}
@@ -16,6 +17,7 @@ const ModalConfirm = ({
onCancel,
children,
title = 'Confirm',
loading,
...props
}: ModalConfirmProps) => {
return (
@@ -25,7 +27,7 @@ const ModalConfirm = ({
<div className="mr-4">
<ButtonCommon onClick={onCancel} type="light"> {cancelText}</ButtonCommon>
</div>
<ButtonCommon onClick={onOk}>{okText}</ButtonCommon>
<ButtonCommon onClick={onOk} loading={loading}>{okText}</ButtonCommon>
</div>
</ModalCommon>
)

View File

@@ -10,7 +10,9 @@ import ItemWishList from '../ItemWishList/ItemWishList'
import LabelCommon from '../LabelCommon/LabelCommon'
import s from './ProductCard.module.scss'
import ProductNotSell from './ProductNotSell/ProductNotSell'
import {useAddProductToCart} from "../../hooks/cart"
import { useCartDrawer } from 'src/components/contexts'
import Router from 'next/router'
export interface ProductCardProps extends ProductCard {
buttonText?: string
isSingleButton?: boolean,
@@ -29,14 +31,41 @@ const ProductCardComponent = ({
imageSrc,
isNotSell,
isSingleButton,
productVariantId,
productVariantName,
activeWishlist
}: ProductCardProps) => {
const {addProduct,loading} = useAddProductToCart()
const { openCartDrawer } = useCartDrawer()
const handleAddToCart = () => {
if(productVariantId){
addProduct({variantId:productVariantId,quantity:1},handleAddToCartCallback)
}
}
const handleAddToCartCallback = () => {
openCartDrawer && openCartDrawer()
}
const handleBuyNowCallback = (success:boolean) => {
if(success){
Router.push(ROUTE.CHECKOUT)
}
}
const handleBuyNow = () => {
if(productVariantId){
addProduct({variantId:productVariantId,quantity:1},handleBuyNowCallback)
}
}
if (isNotSell) {
return <div className={`${s.productCardWarpper} ${s.notSell}`}>
<ProductNotSell name={name} imageSrc={imageSrc} />
</div>
}
return (
<div className={s.productCardWarpper}>
@@ -59,7 +88,7 @@ const ProductCardComponent = ({
<div className={s.cardMidTop}>
<Link href={`${ROUTE.PRODUCT_DETAIL}/${slug}`}>
<a>
<div className={s.productname}>{name} </div>
<div className={s.productname}>{productVariantName} </div>
</a>
</Link>
<div className={s.productWeight}>{weight}</div>
@@ -75,15 +104,15 @@ const ProductCardComponent = ({
{
isSingleButton ?
<div className={s.cardButton}>
<ButtonCommon type="light" icon={<IconBuy />} size='small'>Add to cart</ButtonCommon>
<ButtonCommon type="light" icon={<IconBuy />} size='small' onClick={handleAddToCart}>Add to cart</ButtonCommon>
</div>
:
<>
<div className={s.cardIcon}>
<ButtonIconBuy/>
<div className={s.cardIcon} >
<ButtonIconBuy onClick={handleAddToCart} loading={loading}/>
</div>
<div className={s.cardButton}>
<ButtonCommon type="light" size='small'>{buttonText}</ButtonCommon>
<ButtonCommon type="light" size='small' onClick={handleBuyNow}>{buttonText}</ButtonCommon>
</div>
</>
}

View File

@@ -26,10 +26,6 @@ const QuanittyInput = ({
}: QuanittyInputProps) => {
const [value, setValue] = useState<number>(0)
useEffect(() => {
onChange && onChange(value)
}, [value])
useEffect(() => {
initValue && setValue(initValue)
}, [initValue])
@@ -37,16 +33,20 @@ const QuanittyInput = ({
const onPlusClick = () => {
if (max && value + step > max) {
setValue(max)
onChange && onChange(max)
} else {
setValue(value + step)
onChange && onChange(value + step)
}
}
const onMinusClick = () => {
if (min && value - step < min) {
setValue(min)
onChange && onChange(min)
} else {
setValue(value - step)
onChange && onChange(value - step)
}
}
@@ -54,10 +54,13 @@ const QuanittyInput = ({
let value = Number(e.target.value) || 0
if (min && value < min) {
setValue(min)
onChange && onChange(min)
} else if (max && value > max) {
setValue(max)
onChange && onChange(max)
} else {
setValue(value)
onChange && onChange(value)
}
}

View File

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

View File

@@ -0,0 +1,40 @@
import { useState } from 'react'
import { CommonError } from 'src/domains/interfaces/CommonError'
import rawFetcher from 'src/utils/rawFetcher'
import { AddItemToOrderMutation, AddItemToOrderMutationVariables } from '@framework/schema'
import { errorMapping } from 'src/utils/errrorMapping'
import { useGetActiveOrder } from '.'
import { addItemToOrderMutation } from '@framework/utils/mutations/add-item-to-order-mutation'
const useAddProductToCart = () => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<CommonError | null>(null)
const { mutate } = useGetActiveOrder()
const addProduct = (options:AddItemToOrderMutationVariables,
fCallBack: (isSuccess: boolean, message?: string) => void
) => {
setError(null)
setLoading(true)
rawFetcher<AddItemToOrderMutation>({
query: addItemToOrderMutation ,
variables: options,
})
.then(({ data }) => {
if (data.addItemToOrder.__typename !== "Order") {
throw CommonError.create(errorMapping(data.addItemToOrder.message), data.addItemToOrder.errorCode)
}
mutate()
fCallBack(true)
})
.catch((error) => {
setError(error)
fCallBack(false, error.message)
})
.finally(() => setLoading(false))
}
return { loading, addProduct, error }
}
export default useAddProductToCart

View File

@@ -0,0 +1,23 @@
import { Cart } from '@commerce/types/cart'
import { ActiveOrderQuery } from '@framework/schema'
import { cartFragment } from '@framework/utils/fragments/cart-fragment'
import { normalizeCart } from '@framework/utils/normalize'
import { gql } from 'graphql-request'
import gglFetcher from 'src/utils/gglFetcher'
import useSWR from 'swr'
const query = gql`
query activeOrder {
activeOrder {
...Cart
}
}
${ cartFragment }
`
const useGetActiveOrder = () => {
const { data, ...rest } = useSWR<ActiveOrderQuery>([query], gglFetcher)
return { order: data?.activeOrder ? normalizeCart(data!.activeOrder) : null, ...rest }
}
export default useGetActiveOrder

View File

@@ -0,0 +1,41 @@
import { useState } from 'react'
import { CommonError } from 'src/domains/interfaces/CommonError'
import rawFetcher from 'src/utils/rawFetcher'
import { AdjustOrderLineMutationVariables,AdjustOrderLineMutation, RemoveOrderLineMutation, RemoveOrderLineMutationVariables } from '@framework/schema'
import { errorMapping } from 'src/utils/errrorMapping'
import { useGetActiveOrder } from '.'
import { adjustOrderLineMutation } from '@framework/utils/mutations/adjust-order-line-mutation'
import { removeOrderLineMutation } from '@framework/utils/mutations/remove-order-line-mutation'
const useRemoveProductInCart = () => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<CommonError | null>(null)
const { mutate } = useGetActiveOrder()
const removeProduct = (options:RemoveOrderLineMutationVariables,
fCallBack: (isSuccess: boolean, message?: string) => void
) => {
setError(null)
setLoading(true)
rawFetcher<RemoveOrderLineMutation>({
query: removeOrderLineMutation ,
variables: options,
})
.then(({ data }) => {
if (data.removeOrderLine.__typename !== "Order") {
throw CommonError.create(errorMapping(data.removeOrderLine.message), data.removeOrderLine.errorCode)
}
mutate()
fCallBack(true)
})
.catch((error) => {
setError(error)
fCallBack(false, error.message)
})
.finally(() => setLoading(false))
}
return { loading, removeProduct, error }
}
export default useRemoveProductInCart

View File

@@ -0,0 +1,40 @@
import { useState } from 'react'
import { CommonError } from 'src/domains/interfaces/CommonError'
import rawFetcher from 'src/utils/rawFetcher'
import { AdjustOrderLineMutationVariables,AdjustOrderLineMutation } from '@framework/schema'
import { errorMapping } from 'src/utils/errrorMapping'
import { useGetActiveOrder } from '.'
import { adjustOrderLineMutation } from '@framework/utils/mutations/adjust-order-line-mutation'
const useUpdateProductInCart = () => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<CommonError | null>(null)
const { mutate } = useGetActiveOrder()
const updateProduct = (options:AdjustOrderLineMutationVariables,
fCallBack: (isSuccess: boolean, message?: string) => void
) => {
setError(null)
setLoading(true)
rawFetcher<AdjustOrderLineMutation>({
query: adjustOrderLineMutation ,
variables: options,
})
.then(({ data }) => {
if (data.adjustOrderLine.__typename !== "Order") {
throw CommonError.create(errorMapping(data.adjustOrderLine.message), data.adjustOrderLine.errorCode)
}
mutate()
fCallBack(true)
})
.catch((error) => {
setError(error)
fCallBack(false, error.message)
})
.finally(() => setLoading(false))
}
return { loading, updateProduct, error }
}
export default useUpdateProductInCart

View File

@@ -0,0 +1,47 @@
// import { gql } from 'graphql-request'
import { useMemo, useState } from 'react'
// import useActiveCustomer from './useActiveCustomer'
import { CommonError } from 'src/domains/interfaces/CommonError'
import rawFetcher from 'src/utils/rawFetcher'
import {
CollectionList,
CollectionListOptions,
GetCollectionsQuery,
GetCollectionsQueryVariables,
LoginMutation,
} from '@framework/schema'
import { gql } from 'graphql-request'
import { getCollectionsQuery } from '@framework/utils/queries/get-collections-query'
import useSWR from 'swr'
import gglFetcher from 'src/utils/gglFetcher'
const query = gql`
query getCollections($option: CollectionListOptions) {
collections(options:$option) {
items {
id
name
description
slug
productVariants {
totalItems
}
parent {
id
}
children {
id
}
}
}
}
`
const useGetProductListByCollection = (options: any) => {
const { data, ...rest } = useSWR<GetCollectionsQuery>([query, options], gglFetcher)
return { collections: data?.collections, ...rest }
}
export default useGetProductListByCollection

View File

@@ -0,0 +1,36 @@
import { useState } from "react";
// Hook
export function useLocalStorage<T>(key: string, initialValue: T) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState<T>(() => {
try {
// Get from local storage by key
const item = localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
// console.log(error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value: T | ((val: T) => T)) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
// A more advanced implementation would handle the error case
// console.log(error);
}
};
return [storedValue, setValue] as const;
}

View File

@@ -1,5 +1,7 @@
import React from 'react'
import { CollectionListOptions, GetCollectionsQuery } from '@framework/schema'
import React, { useEffect, useMemo, useState } from 'react'
import { Banner, StaticImage } from 'src/components/common'
import useGetProductListByCollection from 'src/components/hooks/useGetProductListByCollection'
import { ROUTE } from 'src/utils/constanst.utils'
import BannerImgRight from './assets/banner_full.png'
import HomeBannerImg from './assets/home_banner.png'
@@ -11,6 +13,10 @@ interface Props {
}
const HomeBanner = ({ }: Props) => {
// const variables = useMemo(() => {
// return {option: {filter: {name: {eq: "Computers" }}}}
// }, [])
// const {collections} = useGetProductListByCollection(variables)
return (
<div className={s.homeBanner}>
<section className={s.left}>

View File

@@ -1,10 +1,13 @@
import React from 'react'
import { ProductCard } from '@commerce/types/product'
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 HomeCollectionProps {}
interface HomeCollectionProps {
data: ProductCard[]
}
const dataTest = [
{
name: 'Tomato',
@@ -92,39 +95,39 @@ const dataTest = [
},
]
const HomeCollection = (props: HomeCollectionProps) => {
const HomeCollection = ({data}: HomeCollectionProps) => {
return (
<div className="w-full">
<CollectionCarcousel
data={dataTest}
data={data}
itemKey="product-2"
title="VEGGIE"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
category={"veggie"}
/>
<CollectionCarcousel
data={dataTest}
data={data}
itemKey="product-3"
title="VEGGIE"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
category={"veggie"}
/>
<CollectionCarcousel
data={dataTest}
data={data}
itemKey="product-4"
title="VEGGIE"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
category={"veggie"}
/>
<CollectionCarcousel
data={dataTest}
data={data}
itemKey="product-5"
title="VEGGIE"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
category={"veggie"}
/>
<CollectionCarcousel
data={dataTest}
data={data}
itemKey="product-6"
title="VEGGIE"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."

View File

@@ -1,27 +1,18 @@
import React, { useMemo } from 'react';
import React from 'react';
import ProductImgs from './components/ProductImgs/ProductImgs'
import ProductInfo from './components/ProductInfo/ProductInfo'
import s from './ProductInfoDetail.module.scss'
import { Product } from '@commerce/types/product'
import { Collection } from '@framework/schema'
import { getCategoryNameFromCollectionId } from 'src/utils/funtion.utils';
interface Props {
productDetail: Product,
collections: Collection[]
}
const ProductInfoDetail = ({ productDetail, collections }: Props) => {
const dataWithCategoryName = useMemo(() => {
return {
...productDetail,
collection: getCategoryNameFromCollectionId(collections, productDetail.collectionIds ? productDetail.collectionIds[0] : undefined)
}
}, [productDetail, collections])
const ProductInfoDetail = ({ productDetail }: Props) => {
return (
<section className={s.productInfoDetail}>
<ProductImgs productImage={productDetail.images}/>
<ProductInfo productInfoDetail={dataWithCategoryName}/>
<ProductInfo productInfoDetail={productDetail}/>
</section >
)
}

View File

@@ -0,0 +1,37 @@
.warpper{
display: flex;
justify-content: flex-start;
flex-direction: column;
align-items: flex-start;
.name{
margin-bottom: 0.5rem;
margin-top: 0.5rem;
font-size: 2rem;
font-weight: 700;
text-transform: capitalize;
}
.option{
display: flex;
justify-content: flex-start;
align-items: flex-start;
// > button {
// margin-right: 1rem;
// }
}
.button {
margin: 1rem 0;
padding: 0;
div {
padding: 0.8rem 1.6rem;
margin-right: 0.8rem;
background-color: var(--gray);
border-radius: 0.8rem;
cursor: pointer;
&.active {
color: var(--white);
background-color: var(--primary);
}
}
}
}

View File

@@ -0,0 +1,55 @@
import { ProductOption, ProductOptionValues } from '@commerce/types/product'
import classNames from 'classnames'
import React, { useEffect, useState } from 'react'
import { SelectedOptions } from 'src/utils/types.utils'
import s from './ProductDetailOption.module.scss'
interface Props {
option: ProductOption
onChane: (values: SelectedOptions) => void
}
const ProductDetailOption = React.memo(({ option, onChane }: Props) => {
const [selected, setSelected] = useState<string>('')
useEffect(() => {
if (option) {
setSelected(option.values[0].label)
}
}, [option])
const handleClick = (value:string) => {
setSelected(value)
onChane && onChane({[option.displayName]:value})
}
return (
<div className={s.warpper}>
<div className={s.name}>{option.displayName}:</div>
<div className={s.option}>
{option.values.map((value) => {
return <ProductDetailOptionButton value={value} selected={selected} onClick={handleClick} key={value.label}/>
})}
</div>
</div>
)
})
interface ProductDetailOptionButtonProps {
value: ProductOptionValues
selected: string
onClick: (value: string) => void
}
const ProductDetailOptionButton = ({
value,
selected,
onClick,
}: ProductDetailOptionButtonProps) => {
const handleClick = () => {
onClick && onClick(value.label)
}
return (
<div className={s.button}>
<div onClick={handleClick} className={classNames({ [s.active]: selected === value.label })}>
{value.label}
</div>
</div>
)
}
export default ProductDetailOption

View File

@@ -1,8 +1,15 @@
import { Product } from '@commerce/types/product'
import React from 'react'
import Router from 'next/router'
import React, { useEffect, useState } from 'react'
import { ButtonCommon, LabelCommon, QuanittyInput } from 'src/components/common'
import { useCartDrawer, useMessage } from 'src/components/contexts'
import { useAddProductToCart } from 'src/components/hooks/cart'
import { IconBuy } from 'src/components/icons'
import { ROUTE } from 'src/utils/constanst.utils'
import { getProductVariant } from 'src/utils/funtion.utils'
import { LANGUAGE } from 'src/utils/language.utils'
import { SelectedOptions } from 'src/utils/types.utils'
import ProductDetailOption from '../ProductDetailOption/ProductDetailOption'
import s from './ProductInfo.module.scss'
interface Props {
@@ -10,11 +17,73 @@ interface Props {
}
const ProductInfo = ({ productInfoDetail }: Props) => {
console.log(productInfoDetail)
const [option, setOption] = useState({})
const [quanitty, setQuanitty] = useState(0)
const [addToCartLoading, setAddToCartLoading] = useState(false)
const [buyNowLoading, setBuyNowLoading] = useState(false)
const { showMessageSuccess, showMessageError } = useMessage()
useEffect(() => {
let defaultOption:SelectedOptions = {}
productInfoDetail.options.map((option)=>{
defaultOption[option.displayName] = option.values[0].label
return null
})
}, [productInfoDetail])
const {addProduct} = useAddProductToCart()
const { openCartDrawer } = useCartDrawer()
function handleAddToCart() {
setAddToCartLoading(true)
const variant = getProductVariant(productInfoDetail, option)
if (variant) {
addProduct({ variantId: variant.id.toString(), quantity: quanitty }, handleAddToCartCallback)
}else{
setAddToCartLoading(false)
}
}
const handleAddToCartCallback = (isSuccess:boolean,message?:string) => {
setAddToCartLoading(false)
if(isSuccess){
showMessageSuccess("Add to cart successfully!", 4000)
openCartDrawer && openCartDrawer()
}else{
showMessageError(message||"Error")
}
}
const handleBuyNowCallback = (success:boolean,message?:string) => {
setBuyNowLoading(false)
if(success){
Router.push(ROUTE.CHECKOUT)
}else{
showMessageError(message||"Error")
}
}
const handleBuyNow = () => {
setBuyNowLoading(true)
const variant = getProductVariant(productInfoDetail, option)
if (variant) {
addProduct({ variantId: variant.id.toString(), quantity: quanitty }, handleBuyNowCallback)
}else{
setBuyNowLoading(false)
}
}
const handleQuanittyChange = (value:number) => {
setQuanitty(value)
}
const onSelectOption = (value:SelectedOptions) => {
setOption({...option,...value})
// let variant = getProductVariant(productInfoDetail,value)
// console.log(variant)
}
return (
<section className={s.productInfo}>
<div className={s.info}>
<LabelCommon shape='half'>{productInfoDetail.collection}</LabelCommon>
<LabelCommon shape='half'>{productInfoDetail.collection?.[0]}</LabelCommon>
<h2 className={s.heading}>{productInfoDetail.name}</h2>
<div className={s.price}>
<div className={s.old}>
@@ -26,14 +95,22 @@ const ProductInfo = ({ productInfoDetail }: Props) => {
<div className={s.description}>
{productInfoDetail.description}
</div>
<div className={s.options}>
{
productInfoDetail.options.map((option)=>{
return <ProductDetailOption option={option} onChane={onSelectOption} key={option.displayName}/>
})
}
</div>
</div>
<div className={s.actions}>
<QuanittyInput />
<QuanittyInput value={quanitty} onChange={handleQuanittyChange}/>
<div className={s.bottom}>
{/* <ButtonCommon size='large'>{LANGUAGE.BUTTON_LABEL.PREORDER}</ButtonCommon> */}
<ButtonCommon size='large'>{LANGUAGE.BUTTON_LABEL.BUY_NOW}</ButtonCommon>
<ButtonCommon size='large' onClick={handleBuyNow} loading={buyNowLoading} disabled={addToCartLoading}>{LANGUAGE.BUTTON_LABEL.BUY_NOW}</ButtonCommon>
<ButtonCommon size='large' type='light'>
<ButtonCommon size='large' type='light' onClick={handleAddToCart} loading={addToCartLoading} disabled={buyNowLoading}>
<span className={s.buttonWithIcon}>
<IconBuy /><span className={s.label}>{LANGUAGE.BUTTON_LABEL.ADD_TO_CARD}</span>
</span>

View File

@@ -1,5 +1,5 @@
import { Collection } from '@commerce/types/collection';
import { ProductCard } from '@commerce/types/product';
import { Collection } from '@framework/schema';
import React, { useMemo } from 'react';
import ListProductWithInfo from 'src/components/common/ListProductWithInfo/ListProductWithInfo';
import { getCategoryNameFromCollectionId } from 'src/utils/funtion.utils';

View File

@@ -1,13 +1,22 @@
import React from 'react';
import { Product } from '@commerce/types/product';
import React, { useEffect, useState } from 'react';
import ListProductWithInfo from 'src/components/common/ListProductWithInfo/ListProductWithInfo';
import { PRODUCT_DATA_TEST } from 'src/utils/demo-data';
const ViewedProducts = () => {
import { ProductCardProps } from 'src/components/common/ProductCard/ProductCard';
import { LOCAL_STORAGE_KEY } from 'src/utils/constanst.utils'
import { normalizeProductCard } from '@framework/utils/normalize';
import { useLocalStorage } from 'src/components/hooks/useLocalStorage';
interface Props {
data: ProductCardProps[]
}
const ViewedProducts = ({data}:Props) => {
if (data.length===0){
return <div></div>
}
return (
<ListProductWithInfo
title="viewed Products"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
data={PRODUCT_DATA_TEST}
data={data}
hasBorderBottomMobile={true}
/>
);

View File

@@ -0,0 +1,2 @@

View File

@@ -44,7 +44,8 @@ export const ACCOUNT_TAB = {
}
export const LOCAL_STORAGE_KEY = {
TOKEN: 'token'
TOKEN: 'token',
VIEWEDPRODUCT: "viewed-product"
}
export const QUERY_SPLIT_SEPERATOR = ','
@@ -82,46 +83,58 @@ 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`,
},
]
{
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 BRAND = [
{
name: 'Maggi',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=maggi`,
export const FACET = {
FEATURE: {
PARENT_NAME: 'Featured',
FRESH: 'Fresh',
BEST_SELLERS: 'Best seller'
},
{
name: 'Chomilex',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=chomilex`,
},
{
name: 'Chinsu',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=chinsu`,
},
]
CATEGORY: {
PARENT_NAME:"category",
VEGGIE:"veggie"
}
}
export const CODE_FACET_FEATURED = 'featured'
export const CODE_FACET_DISCOUNT = 'discount'

0
src/utils/enum.ts Normal file
View File

View File

@@ -1,7 +1,9 @@
import { Collection } from '@commerce/types/collection';
import { Facet } from "@commerce/types/facet";
import { Collection, FacetValue, SearchResultSortParameter } from './../../framework/vendure/schema.d';
import { CODE_FACET_DISCOUNT, CODE_FACET_FEATURED, CODE_FACET_FEATURED_VARIANT, PRODUCT_SORT_OPTION_VALUE } from "./constanst.utils";
import { PromiseWithKey, SortOrder } from "./types.utils";
import { Product, ProductCard, ProductOption, ProductOptionValues } from "@commerce/types/product";
import { FacetValue, SearchResultSortParameter } from './../../framework/vendure/schema.d';
import { CODE_FACET_DISCOUNT, CODE_FACET_FEATURED, CODE_FACET_FEATURED_VARIANT, FACET, PRODUCT_SORT_OPTION_VALUE } from "./constanst.utils";
import { PromiseWithKey, SelectedOptions, SortOrder } from "./types.utils";
export function isMobile() {
return window.innerWidth < 768
@@ -79,6 +81,18 @@ export function getFreshFacetId(facets: Facet[]) {
return freshFacetValue?.id
}
export function getFacetIdByName(facets: Facet[], facetName: string, valueName:string) {
const featuredFacet = facets.find((item: Facet) => item.name === facetName)
const freshFacetValue = featuredFacet?.values.find((item: FacetValue) => item.name === valueName)
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 || []
}
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)
@@ -127,4 +141,36 @@ export const getCategoryNameFromCollectionId = (colelctions: Collection[], colle
export function getAllPromies(promies: PromiseWithKey[]) {
return promies.map(item => item.promise)
}
export const FilterOneVatiant = (products:ProductCard[]) => {
let idList:string[] = []
let filtedProduct: ProductCard[]=[]
products.map((product:ProductCard)=>{
if(!idList.includes(product.id)){
filtedProduct.push(product)
idList.push(product.id)
}
})
return filtedProduct
}
export const convertOption = (values :ProductOptionValues[]) => {
return values.map((value)=>{ return {name:value.label,value:value.label}})
}
export function getProductVariant(product: Product, opts: SelectedOptions) {
const variant = product.variants?.find((variant) => {
return Object.entries(opts).every(([key, value]) =>
variant.options.find((option) => {
if (
option.__typename === 'MultipleChoiceOption' &&
option.displayName.toLowerCase() === key.toLowerCase()
) {
return option.values.find((v) => v.label.toLowerCase() === value)
}
})
)
})
return variant
}

View File

@@ -1,3 +1,4 @@
export interface ProductProps {
category?: string
name: string
@@ -57,6 +58,14 @@ export type filterContextType = {
close: () => void;
};
export interface StringMap { [key: string]: string; }
export interface FacetMap extends StringMap{
PARENT_NAME: string
}
export interface FacetConstant{
[key: string]: FacetMap;
}
export type PromiseWithKey = {
key: string
promise: PromiseLike<any>
@@ -77,3 +86,4 @@ export type OrderState = | 'Created'
| 'ArrangingAdditionalPayment'
| 'Cancelled'
export type SelectedOptions = Record<string, string | null>