From 833c5205b23f889b5b497520dc3b0dc5a242afe5 Mon Sep 17 00:00:00 2001 From: DatNguyen Date: Thu, 14 Oct 2021 14:17:23 +0700 Subject: [PATCH 1/3] :sparkles: feat: add product to cart :%s --- framework/commerce/types/product.ts | 3 +- .../vendure/api/operations/get-product.ts | 3 +- framework/vendure/schema.d.ts | 2 +- .../utils/queries/get-product-query.ts | 1 + next-env.d.ts | 3 + .../MenuFilterItem/MenuFilterItem.tsx | 1 - .../ProductInfoDetail/ProductInfoDetail.tsx | 14 +-- .../ProductDetailOption.module.scss | 37 ++++++++ .../ProductDetailOption.tsx | 55 ++++++++++++ .../components/ProductInfo/ProductInfo.tsx | 89 +++++++++++++++++-- src/utils/funtion.utils.ts | 24 ++++- src/utils/types.utils.ts | 2 + 12 files changed, 215 insertions(+), 19 deletions(-) create mode 100644 src/components/modules/product-detail/ProductInfoDetail/components/ProductDetailOption/ProductDetailOption.module.scss create mode 100644 src/components/modules/product-detail/ProductInfoDetail/components/ProductDetailOption/ProductDetailOption.tsx diff --git a/framework/commerce/types/product.ts b/framework/commerce/types/product.ts index 5152054a3..eb69371db 100644 --- a/framework/commerce/types/product.ts +++ b/framework/commerce/types/product.ts @@ -48,7 +48,8 @@ export type Product = { options: ProductOption[] facetValueIds?: string[] collectionIds?: string[] - collection?: string, + collection?: string[], + variants?: ProductVariant[] } export type ProductCard = { diff --git a/framework/vendure/api/operations/get-product.ts b/framework/vendure/api/operations/get-product.ts index 0aa761ab0..b7ddc5c42 100644 --- a/framework/vendure/api/operations/get-product.ts +++ b/framework/vendure/api/operations/get-product.ts @@ -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 } diff --git a/framework/vendure/schema.d.ts b/framework/vendure/schema.d.ts index 24119e9e6..ad7662bec 100644 --- a/framework/vendure/schema.d.ts +++ b/framework/vendure/schema.d.ts @@ -3347,7 +3347,7 @@ export type GetProductQuery = { __typename?: 'Query' } & { collections: Array< { __typename?: 'Collection' } & Pick< Collection, - 'id' + 'id'|"name" > > } diff --git a/framework/vendure/utils/queries/get-product-query.ts b/framework/vendure/utils/queries/get-product-query.ts index 6db960a96..2eb7ee278 100644 --- a/framework/vendure/utils/queries/get-product-query.ts +++ b/framework/vendure/utils/queries/get-product-query.ts @@ -41,6 +41,7 @@ export const getProductQuery = /* GraphQL */ ` } collections { id + name } } } diff --git a/next-env.d.ts b/next-env.d.ts index c6643fda1..9bc3dd46b 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,3 +1,6 @@ /// /// /// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/src/components/common/MenuFilter/MenuFilterItem/MenuFilterItem.tsx b/src/components/common/MenuFilter/MenuFilterItem/MenuFilterItem.tsx index eeb96fae1..b694e7da8 100644 --- a/src/components/common/MenuFilter/MenuFilterItem/MenuFilterItem.tsx +++ b/src/components/common/MenuFilter/MenuFilterItem/MenuFilterItem.tsx @@ -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'; diff --git a/src/components/modules/product-detail/ProductInfoDetail/ProductInfoDetail.tsx b/src/components/modules/product-detail/ProductInfoDetail/ProductInfoDetail.tsx index 072c1fd56..556acc9c5 100644 --- a/src/components/modules/product-detail/ProductInfoDetail/ProductInfoDetail.tsx +++ b/src/components/modules/product-detail/ProductInfoDetail/ProductInfoDetail.tsx @@ -12,16 +12,16 @@ interface Props { } const ProductInfoDetail = ({ productDetail, collections }: Props) => { - const dataWithCategoryName = useMemo(() => { - return { - ...productDetail, - collection: getCategoryNameFromCollectionId(collections, productDetail.collectionIds ? productDetail.collectionIds[0] : undefined) - } - }, [productDetail, collections]) + // const dataWithCategoryName = useMemo(() => { + // return { + // ...productDetail, + // collection: getCategoryNameFromCollectionId(collections, productDetail.collectionIds ? productDetail.collectionIds[0] : undefined) + // } + // }, [productDetail, collections]) return (
- +
) } diff --git a/src/components/modules/product-detail/ProductInfoDetail/components/ProductDetailOption/ProductDetailOption.module.scss b/src/components/modules/product-detail/ProductInfoDetail/components/ProductDetailOption/ProductDetailOption.module.scss new file mode 100644 index 000000000..d22660a4a --- /dev/null +++ b/src/components/modules/product-detail/ProductInfoDetail/components/ProductDetailOption/ProductDetailOption.module.scss @@ -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); + } + } + } + +} \ No newline at end of file diff --git a/src/components/modules/product-detail/ProductInfoDetail/components/ProductDetailOption/ProductDetailOption.tsx b/src/components/modules/product-detail/ProductInfoDetail/components/ProductDetailOption/ProductDetailOption.tsx new file mode 100644 index 000000000..0d7e13c84 --- /dev/null +++ b/src/components/modules/product-detail/ProductInfoDetail/components/ProductDetailOption/ProductDetailOption.tsx @@ -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('') + useEffect(() => { + if (option) { + setSelected(option.values[0].label) + } + }, [option]) + const handleClick = (value:string) => { + setSelected(value) + onChane && onChane({[option.displayName]:value}) + } + return ( +
+
{option.displayName}:
+
+ {option.values.map((value) => { + return + })} +
+
+ ) +}) +interface ProductDetailOptionButtonProps { + value: ProductOptionValues + selected: string + onClick: (value: string) => void +} +const ProductDetailOptionButton = ({ + value, + selected, + onClick, +}: ProductDetailOptionButtonProps) => { + const handleClick = () => { + onClick && onClick(value.label) + } + return ( +
+
+ {value.label} +
+
+ ) +} + +export default ProductDetailOption diff --git a/src/components/modules/product-detail/ProductInfoDetail/components/ProductInfo/ProductInfo.tsx b/src/components/modules/product-detail/ProductInfoDetail/components/ProductInfo/ProductInfo.tsx index c3e51d46d..961d8e29b 100644 --- a/src/components/modules/product-detail/ProductInfoDetail/components/ProductInfo/ProductInfo.tsx +++ b/src/components/modules/product-detail/ProductInfoDetail/components/ProductInfo/ProductInfo.tsx @@ -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 (
- {productInfoDetail.collection} + {productInfoDetail.collection?.[0]}

{productInfoDetail.name}

@@ -26,14 +95,22 @@ const ProductInfo = ({ productInfoDetail }: Props) => {
{productInfoDetail.description}
+
+ { + productInfoDetail.options.map((option)=>{ + return + }) + } + +
- +
{/* {LANGUAGE.BUTTON_LABEL.PREORDER} */} - {LANGUAGE.BUTTON_LABEL.BUY_NOW} + {LANGUAGE.BUTTON_LABEL.BUY_NOW} - + {LANGUAGE.BUTTON_LABEL.ADD_TO_CARD} diff --git a/src/utils/funtion.utils.ts b/src/utils/funtion.utils.ts index e1ea6cb50..73cd6094a 100644 --- a/src/utils/funtion.utils.ts +++ b/src/utils/funtion.utils.ts @@ -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 } \ No newline at end of file diff --git a/src/utils/types.utils.ts b/src/utils/types.utils.ts index 7f91711bb..78d813f2f 100644 --- a/src/utils/types.utils.ts +++ b/src/utils/types.utils.ts @@ -71,3 +71,5 @@ export type PromiseWithKey = { promise: PromiseLike keyResult?: string, } + +export type SelectedOptions = Record \ No newline at end of file From d2727c8b6d236e0ed0983249ca4050d75a61b578 Mon Sep 17 00:00:00 2001 From: DatNguyen Date: Thu, 14 Oct 2021 15:00:39 +0700 Subject: [PATCH 2/3] remove comment code --- .../ProductInfoDetail/ProductInfoDetail.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/components/modules/product-detail/ProductInfoDetail/ProductInfoDetail.tsx b/src/components/modules/product-detail/ProductInfoDetail/ProductInfoDetail.tsx index 556acc9c5..64a00f7e6 100644 --- a/src/components/modules/product-detail/ProductInfoDetail/ProductInfoDetail.tsx +++ b/src/components/modules/product-detail/ProductInfoDetail/ProductInfoDetail.tsx @@ -1,23 +1,15 @@ -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 (
From b0e47d2671450346c6407d63795c92e4ffa693c0 Mon Sep 17 00:00:00 2001 From: DatNguyen Date: Thu, 14 Oct 2021 15:25:18 +0700 Subject: [PATCH 3/3] remove colecttion --- pages/product/[slug].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/product/[slug].tsx b/pages/product/[slug].tsx index 2da14a995..b66cfb026 100644 --- a/pages/product/[slug].tsx +++ b/pages/product/[slug].tsx @@ -12,7 +12,7 @@ import { PromiseWithKey } from 'src/utils/types.utils' export default function Slug({ product, relevantProducts, collections }: InferGetStaticPropsType) { return <> - +