How to Implement the Quickly Add to Cart Feature?
Last updated
Was this helpful?
Was this helpful?
const [openVariantModal, setOpenVariantModal] = useState(false);
const [openSizeVariantDesktop, setOpenSizeVariantDesktop] = useState(false);
const [openColorVariantDesktop, setOpenColorVariantDesktop] = useState(false);
const [variantType, setVariantType] = useState('');
const [isBasketDrawerOpen, setIsBasketDrawerOpen] = useState(false);
const variants = product?.extra_data?.variants;
const image_url_one = product?.productimage_set[0]?.image;
const product_brand =
product?.attributes_kwargs?.integration_alt_marka?.label;
const basket_offers_label = product?.basket_offers[0]?.label?.toLowerCase();
const basket_offers_price =
product?.basket_offers[0]?.listing_kwargs?.discounted_total_price;
const variantsFilter = variants?.filter(
(item) =>
item.attribute_key === 'mp_color' ||
item.attribute_key === 'integration_color' ||
item.attribute_key === 'integration_renk'
);
const variantsSizes = variants?.filter(
(item) =>
item.attribute_key === 'mp_size' ||
item.attribute_key === 'integration_beden'
);
const selectableColors =
variantsFilter &&
variantsFilter[0]?.options?.filter((item) => item.is_selectable);
const selectableSizes =
variantsSizes &&
variantsSizes[0]?.options?.filter((item) => item.is_selectable);
useEffect(() => {
setProductNameText(product_name);
}, [product_name]);
useEffect(() => {
setProducPriceText(price);
}, [price]);
const [productNameText, setProductNameText] = useState(product_name);
const [producPriceText, setProducPriceText] = useState(price);
const [productVariantUrl, setProducVariantUrl] = useState(absolute_url);
const [productImage, setProductImage] = useState(image_url_one);
const [swiper, setSwiper] = useState(null);
const [multiImages, setMultiImages] = useState(null);
const [selectedPk, setSelectedPk] = useState<number>(product.pk);
useEffect(() => {
setProducVariantUrl(productVariantUrl);
}, [productVariantUrl]);
const handleBasketDrawer = useCallback(
(pk: number) => {
setSelectedPk(pk);
setIsBasketDrawerOpen(true);
},
[setIsBasketDrawerOpen]
);
<ShowVariants
variantsColors={variantsFilter}
variantsSizes={variantsSizes}
setVariantType={setVariantType}
setOpenVariantModal={setOpenVariantModal}
setOpenColorVariantDesktop={setOpenColorVariantDesktop}
setOpenSizeVariantDesktop={setOpenSizeVariantDesktop}
openSizeVariantDesktop={openSizeVariantDesktop}
openColorVariantDesktop={openColorVariantDesktop}
variantsFilter={variantsFilter}
setProductNameText={setProductNameText}
setProducPriceText={setProducPriceText}
setProductImage={setProductImage}
setProducVariantUrl={setProducVariantUrl}
setMultiImages={setMultiImages}
swiper={swiper}
absolute_url={productVariantUrl}
productPk={product?.pk}
setIsBasketDrawerOpen={handleBasketDrawer}
selectedProduct={product}
/>
{openSizeVariantDesktop && (
<Suspense fallback={<LoaderSpinner />}>
<div className="bg-gray-100 p-2">
<div className="mb-3 text-base xl:text-lg font-bold leading-snug text-secondary">
<span>Quickly Add to Cart</span>
</div>
<VariantModal
key={product?.pk}
item={product}
index={index + 1}
type={'size'}
setIsBasketDrawerOpen={handleBasketDrawer}
/>
</div>
</Suspense>
)}
{!openSizeVariantDesktop && (
<div className="h-full">
<Link
href={productVariantUrl}
data-testid={`${product.pk}-${index}`}
className="flex flex-col mb-3 2xl:mb-[1.125rem]"
key={`${product.pk}-${index}`}
>
<span
className={clsx(
'text-xs text-secondary font-bold leading-snug xl:!leading-6',
'2xl:text-lg 2xl:tracking-tight'
)}
>
{product_brand}
</span>
<span
key={`${product.pk}-${index}`}
className="text-xs font-medium leading-snug text-secondary-100 2xl:text-sm 2xl:leading-5 min-h-[50px] md:min-h-10"
>
{productNameText}
</span>
</Link>
{/* <div>
{retail_price && retail_price > price ? (
<DiscountPrice
price={product_min_quantity ? (+product_min_quantity * +producPriceText) : producPriceText}
retailPrice={product_min_quantity ? (+product_min_quantity * +retail_price) : retail_price}
priceColor={
basket_offers_price ? 'text-secondary' : 'text-primary'
}
/>
) : (
<Price
value={product_min_quantity ? (+product_min_quantity * +producPriceText).toString() : producPriceText}
data-testid="product-price"
className="text-secondary text-sm font-bold leading-snug tracking-tight 2xl:text-lg"
/>
)}
</div> */}
</div>
)}
{variants && (
<Modal
portalId={`size_${product.pk}`}
title={`Farklı ${
variantType === 'size' ? 'Beden' : 'Renk'
} Seçenekleri (${
variantType === 'size'
? selectableSizes?.length
: selectableColors?.length
})`}
open={openVariantModal}
setOpen={setOpenVariantModal}
showCloseButton
className="w-full bottom-0 top-auto transform-none left-0 px-0 pb-5 md:hidden"
// titleClass="text-secondary md:text-lg font-semibold leading-snug py-3 px-4 border-b-0"
>
<Suspense fallback={<LoaderSpinner />}>
<div>
<VariantModal
key={product?.pk}
item={product}
index={index + 1}
type={variantType}
setIsBasketDrawerOpen={handleBasketDrawer}
/>
</div>
</Suspense>
</Modal>
)}
{product.pk && (
<DrawerBasket
offerLabel={basket_offers_label}
offerPrice={basket_offers_price}
open={isBasketDrawerOpen}
productPk={selectedPk}
setOpen={setIsBasketDrawerOpen}
/>
)}import { Image } from '@akinon/next/components/image';
import { Link, Price } from '@theme/components';
interface ProductItem {
is_selectable: boolean;
product: {
absolute_url: string;
productimage_set: { image: string };
name: string;
price: number;
};
}
interface Props {
variant: ProductItem[];
index: number;
}
export const ColorVariant = (props: Props) => {
const { variant, index } = props;
const default_image = '/default-no-image.png'
return (
<div
key={index}
className="flex flex-row items-start justify-start gap-x-2 overflow-scroll pl-4"
>
{variant
?.filter((option) => option.is_selectable)
?.map((productItem, itemIndex) => (
<Link
key={itemIndex}
href={productItem.product.absolute_url}
className="w-[3.75rem] min-w-[3.75rem] h-auto p-0 bg-white border-none flex flex-col gap-y-1"
>
<Image
loading="lazy"
src={productItem.product.productimage_set[0].image || default_image}
alt={productItem.product.name}
title={productItem.product.name}
aspectRatio={1}
width={60}
height={60}
crop="center"
className="flex"
style={{
imageRendering : "-webkit-optimize-contrast",
}}
/>
<Price
value={productItem.product.price}
data-testid="product-price"
className="text-secondary text-xs font-medium leading-snug"
/>
</Link>
))}
</div>
);
};import { Image } from '@akinon/next/components/image';
import { Product } from '@akinon/next/types';
import { Button, Icon, Link } from '@theme/components';
import { useAddProductToBasket } from '@theme/hooks';
import { pushAddToCart } from '@theme/utils/gtm';
interface ColorImage {
is_selectable: boolean;
product: Product;
}
const ShowVariants = ({
variantsSizes,
variantsColors,
setVariantType,
setOpenVariantModal,
setOpenSizeVariantDesktop,
openSizeVariantDesktop,
openColorVariantDesktop,
variantsFilter,
setProductNameText,
setProducPriceText,
setProductImage,
setMultiImages,
swiper,
absolute_url,
productPk,
setIsBasketDrawerOpen,
setProducVariantUrl,
selectedProduct
}) => {
const [addProductToBasket] = useAddProductToBasket();
const sizeClickHandler = (isMobile: boolean = false) => {
const selectableSizes =
variantsSizes &&
variantsSizes[0]?.options.filter((option) => option.is_selectable);
if (!selectableSizes) {
addProductToBasket({
product: productPk,
quantity: 1,
attributes: {},
shouldOpenMiniBasket: false
}).then(() => {
setIsBasketDrawerOpen(productPk);
pushAddToCart(selectedProduct);
});
return;
}
setVariantType('size');
if (isMobile) {
setOpenVariantModal(true);
return;
}
setOpenSizeVariantDesktop(!openSizeVariantDesktop);
};
const selectableColorsLength =
variantsFilter &&
variantsFilter[0]?.options?.filter((item) => item.is_selectable)?.length;
const default_image = '/default-no-image.png';
return (
<>
{variantsColors && variantsColors[0]?.options?.length > 1 && (
<Button
onClick={() => {
setVariantType('color');
setOpenVariantModal(true);
}}
className="absolute bottom-3 left-4 z-10 p-1 bg-white flex flex-row items-center gap-x-1 border-none h-auto md:hidden"
>
<Image
loading="lazy"
src="/colors.png"
alt="colors"
aspectRatio={1}
width={28.5}
height={17}
style={{
imageRendering: '-webkit-optimize-contrast'
}}
/>
<span className="text-xs font-medium leading-snug text-secondary">
{selectableColorsLength}
</span>
</Button>
)}
<Button
onClick={() => sizeClickHandler(true)}
className="absolute bottom-3 right-4 z-10 p-1.5 bg-secondary flex flex-row items-center gap-x-1 rounded-full border-none h-auto xl:p-2 hover:bg-secondary md:hidden"
>
<Icon
name={'add'}
className="text-white text-xs !leading-none xl:text-base"
/>
</Button>
<div className="hidden md:flex absolute bottom-[1rem] md:bottom-2 h-4 z-10 right-4">
<Button
onClick={() => sizeClickHandler()}
className="bg-secondary flex flex-row items-center gap-x-1 rounded-full border-none h-auto ml-auto p-2 hover:bg-secondary hover:!text-secondary"
>
<Icon
name={'add'}
className="text-xs text-inherit !leading-none xl:text-base"
/>
</Button>
</div>
{openColorVariantDesktop && variantsFilter?.length && (
<div className="absolute bottom-0 z-10 w-full bg-white flex flex-row items-center justify-start gap-x-1 p-3">
{variantsFilter &&
variantsFilter[0]?.options
?.filter((option: ColorImage) => option.is_selectable)
?.map(
(colorImage: ColorImage, colorImageIndex: number) =>
colorImageIndex < 4 && (
<Image
key={colorImageIndex}
loading="lazy"
src={
colorImage.product.productimage_set[0].image ||
default_image
}
alt={colorImage.product.name}
title={colorImage.product.name}
aspectRatio={1}
width={48}
height={48}
crop="center"
className="flex cursor-pointer"
style={{
imageRendering: '-webkit-optimize-contrast'
}}
onClick={() => {
setProductNameText(colorImage.product.name);
setProducPriceText(colorImage.product.price);
setProductImage(
colorImage.product.productimage_set[0].image ||
default_image
);
setProducVariantUrl(colorImage.product.absolute_url);
setMultiImages(colorImage.product.productimage_set);
swiper?.slideTo(0);
}}
/>
)
)}
{variantsFilter && variantsFilter[0]?.options?.length >= 5 && (
<Link href={absolute_url} className="flex">
<div className="flex flex-row items-center justify-center gap-x-1 w-[4.25rem]">
<span className="text-xs font-medium leading-6 text-secondary underline underline-offset-4">
+4
</span>
<Icon name="chevron-right" size={13} />
</div>
</Link>
)}
</div>
)}
</>
);
};
export default ShowVariants;import { Button } from '@theme/components';
import clsx from 'clsx';
import { useAddProductToBasket } from '@theme/hooks';
import { pushAddToCart } from '@theme/utils/gtm';
import { Product } from '@akinon/next/types';
interface VariantItem {
is_selectable: boolean;
value: string;
label: string;
product: Product;
order: string;
}
interface Props {
variant: VariantItem[];
index: number;
setIsBasketDrawerOpen: (pk: number) => void;
productMinQuantity?: number;
}
export const checkSizeHasComma = (size: string) => {
if (!size) return;
return size?.includes(',') ? size.replace(',', '.') : size;
};
export const SizeVariant = (props: Props) => {
const { variant, index, setIsBasketDrawerOpen, productMinQuantity } = props;
const [addProductToBasket, { isLoading: isAddProductToBasketLoading }] =
useAddProductToBasket();
const handleOnClick = (selectedProduct) => {
addProductToBasket({
product: selectedProduct.pk,
quantity: productMinQuantity ?? 1,
attributes: {},
shouldOpenMiniBasket: false
}).then(() => {
setIsBasketDrawerOpen(selectedProduct.pk);
pushAddToCart(selectedProduct);
localStorage?.setItem(
`GA4_index_${selectedProduct.pk}`,
JSON.stringify(index)
);
});
};
return (
<div
key={index}
className={clsx(
'sm:grid sm:grid-cols-4 lg:flex lg:flex-row items-start justify-start lg:flex-wrap gap-x-5 gap-y-3',
'md:gap-y-1.5 md:gap-x-1'
)}
>
{variant
.filter((option) => option.is_selectable)
.sort((a, b) => {
const filterableSizeA = a?.product?.attributes
?.filterable_size as string;
const filterableSizeB = b?.product?.attributes
?.filterable_size as string;
if (
filterableSizeA &&
filterableSizeB &&
parseFloat(filterableSizeA) &&
parseFloat(filterableSizeB)
) {
const floatA = parseFloat(checkSizeHasComma(filterableSizeA));
const floatB = parseFloat(checkSizeHasComma(filterableSizeB));
return floatA - floatB;
} else if (Number(a?.order) && Number(b?.order)) {
return parseFloat(a.order) - parseFloat(b.order);
}
return 0;
})
?.map((productItem, itemIndex) => (
<Button
key={itemIndex}
type="submit"
appearance="ghost"
value={productItem.value}
onClick={() => handleOnClick(productItem.product)}
disabled={
!productItem.product.in_stock || isAddProductToBasketLoading
}
className={clsx(
'text-sm text-secondary px-0 h-auto w-[2.875rem]',
'hover:bg-secondary hover:text-white hover:border-none hover:rounded',
'md:w-[3.125rem]',
!productItem.product.in_stock &&
'hover:bg-gray-200 hover:text-gray-600'
)}
data-testid="list-variant-add-cart"
>
{productItem?.product?.attributes_kwargs?.filterable_size.label}
</Button>
))}
</div>
);
};import { useGetProductByPkQuery } from '@akinon/next/data/client/product';
import { Product } from '@akinon/next/types';
import { ColorVariant } from './color-variant';
import { SizeVariant } from './size-variant';
import { LoaderSpinner } from '@theme/components';
interface Props {
item: Product;
index: number;
type: string;
setIsBasketDrawerOpen: (pk: number) => void;
}
export const VariantModal = (props: Props) => {
const { item, type, index, setIsBasketDrawerOpen } = props;
const { data, isSuccess , isLoading } = useGetProductByPkQuery(item?.pk);
const product_min_quantity = data?.product?.attributes?.integration_min_quantity;
let variantData = [];
type === 'size'
? (variantData = data?.variants?.filter(
(item) =>
item.attribute_key === 'mp_size' ||
item.attribute_key === 'integration_beden'
))
: (variantData = data?.variants?.filter(
(item) =>
item.attribute_key === 'mp_color' ||
item.attribute_key === 'integration_color' ||
item.attribute_key === 'integration_renk'
));
if(isLoading) {
return <div><LoaderSpinner/></div>
}
return (
isSuccess &&
variantData &&
variantData?.map((variant, i) => (
<div key={i}>
{type === 'color' && (
<ColorVariant key={i} variant={variant.options} index={i} />
)}
{type === 'size' && (
<SizeVariant
key={i}
variant={variant.options}
index={index}
setIsBasketDrawerOpen={setIsBasketDrawerOpen}
productMinQuantity={product_min_quantity}
/>
)}
</div>
))
);
};import { Image } from '@akinon/next/components/image';
import { Modal, LoaderSpinner, Icon, Link } from '@theme/components';
import { useGetProductByParamsQuery } from '@akinon/next/data/client/product';
import { Price } from '@theme/components';
import { ROUTES } from '@theme/routes';
import { useMediaQuery } from '@akinon/next/hooks';
import clsx from 'clsx';
type Props = {
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
productPk: number;
offerLabel?: string | null;
offerPrice?: number | null;
};
const DrawerBasket = (props: Props) => {
const matches = useMediaQuery('(max-height: 670px)');
const { data, isFetching, isSuccess } = useGetProductByParamsQuery(
{
pk: props.productPk
},
{
skip: !props.open
}
);
const product_min_quantity =
data?.product?.attributes?.integration_min_quantity;
const title = (
<div className="flex items-center gap-3 text-2xl text-success font-semibold tracing-[-0.48px] leading-snug">
<Icon name="tick-circle" size={24} />
<span>It is in the bag</span>
</div>
);
return (
<>
<Modal
portalId="basket-drawer"
title={title}
open={props.open}
setOpen={props.setOpen}
className={
'!w-full md:!w-[820px] !max-w-[510px] right-0 left-auto !transform-none md:!transform md:translate-x-0 h-[95%] md:h-screen !rounded-none overflow-auto md:!px-0'
}
>
{isFetching && <LoaderSpinner />}
{isSuccess && (
<div className="mb-8">
<div className="!md:pt-8 md:px-[52px]">
<div className="flex items-center justify-between text-secondary text-sm mb-8 w-full gap-3 md:gap-5">
<div className="w-[110px] h-[110px] md:w-[184px] md:h-[184px] md:max-w-[184px]">
<Image
alt={data.product.name}
src={data.product.productimage_set[0].image}
aspectRatio={184 / 184}
fill
sizes="768px"
className="w-[110px] h-[110px] md:w-[184px] md:h-[184px]"
style={{
imageRendering: '-webkit-optimize-contrast'
}}
/>
</div>
<div className="text-xs md:text-sm">
<p className="font-medium mb-3">
<span className="font-bold">
{data.product.attributes_kwargs.integration_alt_marka
?.label ||
data.product.attributes.integration_alt_marka}
</span>{' '}
{data.product.name}
</p>
<div className="flex justify-between mb-3 md:mb-4">
<p>
Renk:{' '}
<span className="font-bold">
{data.product.attributes.integration_renk_tonu}
</span>{' '}
</p>
<p className="hidden md:block">
Beden:{' '}
<span className="font-bold">
{data.product.attributes_kwargs.filterable_size?.value}
</span>{' '}
</p>
</div>
<p className="font-bold text-sm md:text-lg leading-6 tracking-tighter">
{parseFloat(data.product.retail_price) >
parseFloat(data.product.price) && (
<Price
value={parseFloat(
product_min_quantity
? (
+product_min_quantity *
+data.product.retail_price
).toString()
: data.product.retail_price
)}
decimalScale={2}
className="line-through text-base font-medium leading-6 text-secondary"
/>
)}
<Price
value={
product_min_quantity
? (
+product_min_quantity * +data.product.price
).toString()
: data.product.price
}
decimalScale={2}
className={clsx(
'text-lg font-semibold leading-snug',
parseFloat(data.product.retail_price) >
parseFloat(data.product.price) && 'text-primary ml-4'
)}
/>
</p>
{props.offerPrice && props.offerLabel && (
<div className="flex flex-col mt-1 2xl:flex-row 2xl:items-center 2xl:gap-x-1 2xl:mt-1.5">
<span className="text-xs text-secondary whitespace-nowrap font-medium leading-snug capitalize 2xl:leading-6">
{props.offerLabel}
</span>
<Price
value={props.offerPrice}
data-testid="product-price"
className="text-sm text-primary whitespace-nowrap font-bold leading-snug 2xl:tracking-tight"
/>
</div>
)}
</div>
</div>
<div
style={{
boxShadow: matches
? '0 -6px 6px 0 rgba(0, 0, 0, 0.08)'
: 'none'
}}
className="p-4 md:p-0 fixed bottom-0 left-0 w-full md:static bg-white z-10 md:!shadow-none flex justify-between items-center md:mb-24"
>
<button
onClick={() => props.setOpen(false)}
className="font-medium underline"
>
Continue Shopping
</button>
<Link
as="a"
href={ROUTES.BASKET}
className="font-medium w-[151px] md:w-[214px] text-center"
>
Go to Bag
</Link>
</div>
</div>
</div>
)}
</Modal>
</>
);
};
export default DrawerBasket;