Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add quantity input on product page in demo store #1753

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
91 changes: 91 additions & 0 deletions templates/demo-store/app/components/QuantityInput.tsx
@@ -0,0 +1,91 @@
import clsx from 'clsx';
import {ChangeEvent, useEffect, useMemo, useState} from 'react';

export function QuantityInput({
label = '',
className = '',
maxValue = null,
minValue = 0,
setValue,
}: {
label?: string;
className?: string;
maxValue?: number | null;
minValue?: number;
setValue: (value: number) => void;
}) {
const [quantity, setQuantity] = useState(minValue);

useEffect(() => {
setValue(quantity);
}, [quantity]);

const setDecrease = () => {
quantity > minValue ? setQuantity(quantity - 1) : setQuantity(minValue);
};

const setIncrease = () => {
if (maxValue !== null) {
quantity < maxValue ? setQuantity(quantity + 1) : setQuantity(maxValue);
return;
}
setQuantity(quantity + 1);
};

const inputHandler = (event: ChangeEvent<HTMLInputElement>) => {
let value = Number((event.target as HTMLInputElement).value);
if (isNaN(value)) return;

if (value < minValue) {
value = minValue;
}

if (maxValue !== null && value > maxValue) {
value = maxValue;
}

setQuantity(value);
};

const decreaseDisabled = useMemo(() => {
return quantity <= minValue;
}, [minValue, quantity]);

const increaseDisabled = useMemo(() => {
return maxValue ? quantity >= maxValue : false;
}, [maxValue, quantity]);

return (
<div className={className}>
{label && <div className="text-primary">{label}</div>}
<div className="flex justify-between items-center w-full rounded border border-thin bg-transparent mt-1">
<button
className={clsx(
'text-2xl px-4 py-1 rounded-l hover:bg-primary/10 active:bg-primary/5 transition-all duration-200',
decreaseDisabled ? 'text-primary/30' : 'text-primary/70',
)}
onClick={() => setDecrease()}
disabled={decreaseDisabled}
>
<span>&#8722;</span>
</button>
<input
value={quantity}
onChange={inputHandler}
min="1"
className="text-primary text-center border-none bg-transparent w-full"
/>
<button
className={clsx(
'text-2xl px-4 py-1 rounded-r hover:bg-primary/10 active:bg-primary/5 transition-all duration-200',
increaseDisabled ? 'text-primary/30' : 'text-primary/70',
)}
onClick={() => setIncrease()}
disabled={increaseDisabled}
>
<span>&#43;</span>
</button>
</div>
</div>
);
}
1 change: 1 addition & 0 deletions templates/demo-store/app/components/index.ts
Expand Up @@ -21,4 +21,5 @@ export {SortFilter} from './SortFilter';
export {Grid} from './Grid';
export {FeaturedProducts} from './FeaturedProducts';
export {AddToCartButton} from './AddToCartButton';
export {QuantityInput} from './QuantityInput';
export * from './Icon';
@@ -1,4 +1,4 @@
import {useRef, Suspense} from 'react';
import {useRef, Suspense, useState} from 'react';
import {Disclosure, Listbox} from '@headlessui/react';
import {defer, redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {useLoaderData, Await} from '@remix-run/react';
Expand Down Expand Up @@ -30,6 +30,7 @@ import {
Link,
AddToCartButton,
Button,
QuantityInput,
} from '~/components';
import {getExcerpt} from '~/lib/utils';
import {seoPayload} from '~/lib/seo.server';
Expand Down Expand Up @@ -217,6 +218,8 @@ export function ProductForm({

const closeRef = useRef<HTMLButtonElement>(null);

const [productQuantity, setProductQuantity] = useState(1);

/**
* Likewise, we're defaulting to the first variant for purposes
* of add to cart if there is none returned from the loader.
Expand All @@ -232,7 +235,31 @@ export function ProductForm({

const productAnalytics: ShopifyAnalyticsProduct = {
...analytics.products[0],
quantity: 1,
quantity: productQuantity,
};

/**
* There's an additional permission: unauthenticated_read_product_inventory access inventory.
* https://shopify.dev/docs/api/usage/access-scopes
* You'll need to configure this from Storefront settings in your Hydrogen application.
* If you have enabled unauthenticated_read_product_inventory permission your can enable 'quantityAvailable' field
* in PRODUCT_VARIANT_FRAGMENT of PRODUCT_QUERY otherwise leave it disabled to avoid error
*/
const quantityAvailable: number | null =
(selectedVariant as any).quantityAvailable || null;

const priceWithQuantity = {
amount: String(
Number(selectedVariant?.price?.amount || 0) * productQuantity,
),
currencyCode: selectedVariant?.price?.currencyCode,
};

const compareAtPriceWithQuantity = {
amount: String(
Number(selectedVariant?.compareAtPrice?.amount || 0) * productQuantity,
),
currencyCode: selectedVariant?.compareAtPrice?.currencyCode,
};

return (
Expand Down Expand Up @@ -335,6 +362,14 @@ export function ProductForm({
</VariantSelector>
{selectedVariant && (
<div className="grid items-stretch gap-4">
{!isOutOfStock && (
<QuantityInput
label="Quantity"
maxValue={quantityAvailable}
minValue={1}
setValue={setProductQuantity}
/>
)}
{isOutOfStock ? (
<Button variant="secondary" disabled>
<Text>Sold out</Text>
Expand All @@ -344,7 +379,7 @@ export function ProductForm({
lines={[
{
merchandiseId: selectedVariant.id!,
quantity: 1,
quantity: productQuantity,
},
]}
variant="primary"
Expand All @@ -361,14 +396,14 @@ export function ProductForm({
<span>Add to Cart</span> <span>·</span>{' '}
<Money
withoutTrailingZeros
data={selectedVariant?.price!}
data={priceWithQuantity!}
as="span"
data-test="price"
/>
{isOnSale && (
<Money
withoutTrailingZeros
data={selectedVariant?.compareAtPrice!}
data={compareAtPriceWithQuantity!}
as="span"
className="opacity-50 strike"
/>
Expand Down Expand Up @@ -443,6 +478,9 @@ const PRODUCT_VARIANT_FRAGMENT = `#graphql
fragment ProductVariantFragment on ProductVariant {
id
availableForSale
# If you have enabled unauthenticated_read_product_inventory permission your can enable 'quantityAvailable' field
# otherwise leave it disabled to avoid error
#quantityAvailable
selectedOptions {
name
value
Expand Down