mirror of
https://github.com/vercel/commerce.git
synced 2025-07-22 12:24:18 +00:00
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:
@@ -48,7 +48,8 @@ export type Product = {
|
||||
options: ProductOption[]
|
||||
facetValueIds?: string[]
|
||||
collectionIds?: string[]
|
||||
collection?: string,
|
||||
collection?: string[],
|
||||
variants?: ProductVariant[]
|
||||
}
|
||||
|
||||
export type ProductCard = {
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
2
framework/vendure/schema.d.ts
vendored
2
framework/vendure/schema.d.ts
vendored
@@ -3425,7 +3425,7 @@ export type GetProductQuery = { __typename?: 'Query' } & {
|
||||
collections: Array<
|
||||
{ __typename?: 'Collection' } & Pick<
|
||||
Collection,
|
||||
'id'
|
||||
'id'|"name"
|
||||
>
|
||||
>
|
||||
}
|
||||
|
@@ -41,6 +41,7 @@ export const getProductQuery = /* GraphQL */ `
|
||||
}
|
||||
collections {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
3
next-env.d.ts
vendored
3
next-env.d.ts
vendored
@@ -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.
|
||||
|
@@ -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}/>
|
||||
|
@@ -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';
|
||||
|
@@ -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 >
|
||||
)
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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
|
@@ -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>
|
||||
|
@@ -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
|
||||
}
|
@@ -71,3 +71,5 @@ export type PromiseWithKey = {
|
||||
promise: PromiseLike<any>
|
||||
keyResult?: string,
|
||||
}
|
||||
|
||||
export type SelectedOptions = Record<string, string | null>
|
Reference in New Issue
Block a user