Merge pull request #84 from KieIO/feature/m3-add-product-to-cart-product-detail

Feature/m3 add product to cart product detail
This commit is contained in:
datnguyenkieio
2021-10-18 11:42:06 +07:00
committed by GitHub
13 changed files with 212 additions and 24 deletions

View File

@@ -48,7 +48,8 @@ export type Product = {
options: ProductOption[]
facetValueIds?: string[]
collectionIds?: string[]
collection?: string,
collection?: string[],
variants?: ProductVariant[]
}
export type ProductCard = {

View File

@@ -54,7 +54,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

@@ -3425,7 +3425,7 @@ export type GetProductQuery = { __typename?: 'Query' } & {
collections: Array<
{ __typename?: 'Collection' } & Pick<
Collection,
'id'
'id'|"name"
>
>
}

View File

@@ -41,6 +41,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

@@ -12,7 +12,7 @@ import { PromiseWithKey } from 'src/utils/types.utils'
export default function Slug({ product, relevantProducts, collections }: InferGetStaticPropsType<typeof getStaticProps>) {
return <>
<ProductInfoDetail productDetail={product} collections={collections}/>
<ProductInfoDetail productDetail={product}/>
<RecipeDetail ingredients={INGREDIENT_DATA_TEST} />
<RecommendedRecipes data={RECIPE_DATA_TEST} />
<ReleventProducts data={relevantProducts} collections={collections}/>

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

@@ -1,27 +1,19 @@
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,8 +1,8 @@
import { Facet } from "@commerce/types/facet";
import { ProductCard } from "@commerce/types/product";
import { Product, ProductCard, ProductOption, ProductOptionValues } from "@commerce/types/product";
import { Collection, 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, SortOrder } from "./types.utils";
import { PromiseWithKey, SelectedOptions, SortOrder } from "./types.utils";
export function isMobile() {
return window.innerWidth < 768
@@ -152,4 +152,24 @@ export const FilterOneVatiant = (products:ProductCard[]) => {
}
})
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

@@ -71,3 +71,5 @@ export type PromiseWithKey = {
promise: PromiseLike<any>
keyResult?: string,
}
export type SelectedOptions = Record<string, string | null>