Create a directory under components like components/opt7
.
components
├── opt7
│ └── sticky-atc
│ └── index.tsx
'use client';
import { ShoppingCart, Loader2 as Spinner } from 'lucide-react';
import { useTranslations } from 'next-intl';
import React, { useEffect, useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { getProduct } from '~/client/queries/get-product';
import { BcImage } from '~/components/bc-image';
import { QuantityField } from '~/components/product-form/fields/quantity-field';
import { Button } from '~/components/ui/button';
type Product = Awaited<ReturnType<typeof getProduct>>;
const StickyATC = ({ product, id }: { product: NonNullable<Product>; id: string }) => {
const t = useTranslations('Product.Form');
const { formState } = useFormContext();
const { isSubmitting } = formState;
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const handleScroll = () => {
const element = document.getElementById(id);
if (element) {
const bounding = element.getBoundingClientRect();
const isInViewport =
bounding.top >= 0 &&
bounding.left >= 0 &&
bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
bounding.right <= (window.innerWidth || document.documentElement.clientWidth);
setIsVisible(!isInViewport);
}
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [id]);
return (
<div
className={`fixed bottom-0 left-1/2 z-50 flex h-0 w-full -translate-x-1/2 transform bg-white shadow-[-28px_10px_80px_-19px_#000] transition-all duration-300 ${isVisible ? 'flex h-[70px]' : 'hidden'}`}
id={id}
>
<div className="product-properties absolute inset-0 m-auto flex h-full w-full justify-between gap-[6px] px-[16px] py-[8px]">
<div className="image hidden h-[54px] w-[54px] rounded border border-gray-500 lg:block">
{product.defaultImage ? (
<BcImage
alt={product.defaultImage.altText}
className="h-full object-contain"
height={54}
priority={true}
src={product.defaultImage.url}
width={54}
/>
) : null}
</div>
<div className="title grid w-auto place-items-center text-[#0f172a]">
<h2 className="m-0 line-clamp-3 overflow-hidden p-0 text-15 font-semibold">
{product.name}
</h2>
</div>
<div className="addToCart flex w-[140px] min-w-[140px] items-center justify-end gap-[40px] md:min-w-[280px]">
<QuantityField showLabel={false} />
<Button
className="mx-0 h-[40px] w-[44px] max-w-56 rounded px-3 py-0 text-15 font-bold leading-[1] text-white hover:border-transparent hover:bg-primary sm:text-15 md:min-w-[123px]"
disabled={product.availabilityV2.status === 'Unavailable' || isSubmitting}
type="submit"
>
{isSubmitting ? (
<>
<Spinner aria-hidden="true" className="animate-spin" />
<span className="sr-only">{t('processing')}</span>
</>
) : (
<div className="flex items-center gap-1">
<div className="hidden md:block">{t('addToCart')}</div>
<ShoppingCart size={16} />
</div>
)}
</Button>
</div>
</div>
</div>
);
};
export default StickyATC;
Add this component to product form component
components
├── product-form
│ └── index.tsx
.
.
import StickyATC from '../opt7/sticky-atc';
.
.
export const ProductForm = ({
product,
showPriceRange,
}: {
product: Product;
showPriceRange: boolean;
}) => {
.
.
.
return (
.
.
<FormProvider handleSubmit={handleSubmit} register={register} {...methods}>
.
.
.
<StickyATC id="opt7ATC" product={product} />
</FormProvider>
)
}
** The id we specified is the id of the add to cart button in the product form.
Happy Hacking! 🧡