Skip to content

Commit

Permalink
Feature/slider (#6)
Browse files Browse the repository at this point in the history
* Fix prices on slider

* Add to cart from carousel

* Update Slider

* Update useProducts
  • Loading branch information
usulpro authored Dec 27, 2023
1 parent bd22997 commit dca31a2
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 93 deletions.
71 changes: 22 additions & 49 deletions apps/web/components/ProductSlider/ProductSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,37 @@ import React from 'react';
import { SfScrollable } from '@storefront-ui/react';
import { ProductCard } from '~/components';
import type { ProductSliderProps } from '~/components';
import { sdk } from '~/sdk';
import { useProducts } from '~/hooks';

const processProducts = (resp) => {
try {
const productsResponse = resp.data.products.edges;
return productsResponse.map(({ node }) => ({
...node,
price: node.priceRange?.minVariantPrice?.amount,
currencyCode: node.priceRange?.minVariantPrice?.currencyCode,
}));
} catch (error) {
console.error(error);
return [];
}
};
export function ProductSlider({ className, collection, ...attributes }: ProductSliderProps) {
const { products } = useProducts(collection);

const useProducts = () => {
const [products, setProducts] = React.useState([]);

const getData = async () => {
const resp = await sdk.shopify.getProducts({});
setProducts(resp.products);
};

React.useEffect(() => {
getData();
}, []);

return { products };
};

export function ProductSlider({ className, ...attributes }: ProductSliderProps) {
const { products } = useProducts();

// return products.map((d) => <p>{JSON.stringify(d)}</p>);
return (
<SfScrollable
buttonsPlacement="floating"
className="items-center pb-4"
{...attributes}
wrapperClassName={className}
>
{products.map(({ id, title, description, rating, price, currencyCode, primaryImage, slug }) => (
<ProductCard
key={id}
className="w-[192px]"
descriptionClassName="h-[192px]"
name={title}
description={description}
ratingCount={rating?.count}
rating={rating?.average}
price={price}
currencyCode={currencyCode}
imageUrl={primaryImage?.url}
imageAlt={primaryImage?.altText}
slug={slug}
/>
))}
{products.map((product) => {
const { id, title, description, priceRange, primaryImage, slug, variants } = product;
return (
<ProductCard
key={id}
className="w-[192px]"
descriptionClassName="h-[192px]"
name={title}
description={description}
compareAtPrice={variants[0]?.compareAtPrice?.amount}
price={priceRange.minVariantPrice.amount}
currencyCode={priceRange.minVariantPrice.currencyCode}
imageUrl={primaryImage?.url}
imageAlt={primaryImage?.altText}
slug={slug}
product={product}
/>
);
})}
</SfScrollable>
);
}
4 changes: 1 addition & 3 deletions apps/web/components/ProductSlider/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { SfProduct } from '@vue-storefront/unified-data-model';

export type ProductSliderProps = {
products: SfProduct[];
collection?: string;
className?: string;
};
4 changes: 1 addition & 3 deletions apps/web/components/RenderContent/RenderContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ export function RenderContent({ content, ...attributes }: RenderContentProps): J
return <Display items={fields.items} />;
}
case 'ProductSlider': {
return (
<ProductSlider products={fields.items} className="max-w-screen-3xl mx-auto px-4 md:px-10 mb-20" />
);
return <ProductSlider collection="homepage" className="max-w-screen-3xl mx-auto px-4 md:px-10 mb-20" />;
}
case 'Page': {
return <Page />;
Expand Down
60 changes: 48 additions & 12 deletions apps/web/components/ui/ProductCard/ProductCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,50 @@ import { SfButton, SfRating, SfCounter, SfLink, SfIconShoppingCart } from '@stor
import classNames from 'classnames';
import { useTranslation } from 'next-i18next';
import type { ProductCardProps } from '~/components';
import { useCartContext } from '~/hooks';
import useProductRating from '~/hooks/useProductRating/useProductRating';

export function ProductCard({
name,
description,
imageUrl,
imageAlt,
price,
compareAtPrice,
currencyCode,
rating,
ratingCount,
slug,
className,
descriptionClassName,
priority,
product,
...attributes
}: ProductCardProps) {
const { t } = useTranslation();
const { rating, ratingCount } = useProductRating(product);
const { addCustomVariantToCart, isLoading } = useCartContext();

// TODO [>0.2] Care about getting right locale for price formatting
const priceString = price
? new Intl.NumberFormat('en-EN', { style: 'currency', currency: currencyCode }).format(price)
: '';
const compareString = compareAtPrice
? new Intl.NumberFormat('en-EN', { style: 'currency', currency: currencyCode }).format(compareAtPrice)
: '';

const handleAddToCart = () => {
if (isLoading) {
return;
}
const variantId = product?.variants[0].id;
addCustomVariantToCart(1, variantId);
};

return (
<div
className={classNames('border border-neutral-200 rounded-md hover:shadow-lg flex-auto flex-shrink-0', className)}
className={classNames(
'border border-neutral-200 rounded-md hover:shadow-lg flex-auto flex-shrink-0 flex flex-col justify-between',
className,
)}
data-testid="product-card"
{...attributes}
>
Expand Down Expand Up @@ -64,15 +84,31 @@ export function ProductCard({
</SfLink>
</div>
) : null}
<p className="block py-2 font-normal typography-text-xs text-neutral-700 text-justify flex-grow max-h-24 overflow-hidden text-ellipsis">
{description}
</p>
<span className="block pb-2 font-bold typography-text-sm" data-testid="product-card-vertical-price">
{priceString}
</span>
<SfButton type="button" size="sm" slotPrefix={<SfIconShoppingCart size="sm" />}>
{t('addToCartShort')}
</SfButton>
<div className="block py-2 font-normal typography-text-xs text-neutral-700 text-justify flex-grow flex-shrink overflow-hidden">
<p>{description}</p>
</div>
<div>
{compareAtPrice ? (
<p className="pb-2">
<span className="font-bold typography-text-sm" data-testid="product-card-vertical-price">
{priceString}
</span>
<span
className="ml-3 font-normal line-through typography-text-sm"
data-testid="product-card-vertical-price"
>
{compareString}
</span>
</p>
) : (
<span className="block pb-2 font-bold typography-text-sm" data-testid="product-card-vertical-price">
{priceString}
</span>
)}
<SfButton onClick={handleAddToCart} type="button" size="sm" slotPrefix={<SfIconShoppingCart size="sm" />}>
{t('addToCartShort')}
</SfButton>
</div>
</div>
</div>
);
Expand Down
7 changes: 5 additions & 2 deletions apps/web/components/ui/ProductCard/types.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { Maybe } from '@vue-storefront/unified-data-model';
import { Product } from '~/sdk/shopify/types';

export type ProductCardProps = {
name: Maybe<string>;
description?: Maybe<string>;
imageUrl?: Maybe<string>;
imageAlt?: Maybe<string>;
rating?: number;
ratingCount?: number;
price?: number;
compareAtPrice?: number;
currencyCode?: string;
slug?: string;
className?: string;
priority?: boolean;
descriptionClassName: string;
product: Product;
};
55 changes: 48 additions & 7 deletions apps/web/hooks/useCart/useCart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { sdk } from '~/sdk';
import { Product, Variant, CartDetails } from '~/sdk/shopify/types';

type Options = Record<string, string>;
interface CartContextType {
cartId: string | null;
selectedVariantId: string | null;
cart: CartDetails | null;
addOrUpdateCartItem: (quantity: number) => void;
removeCartItem: (lineId: string) => void;
changeCartItemQuantity: (lineId: string, quantity: number) => void;
setSelectedVariant: (selectedOptions: Record<string, string>) => void;
setSelectedVariant: (selectedOptions: Options) => void;
addSelectedVariantToCart: (quantity: number) => void;
addCustomVariantToCart: (quantity: number, variantId: string) => void;
isLoading: boolean;
totalItems: { count: number; lines?: number };
}
Expand Down Expand Up @@ -41,10 +43,7 @@ function calcTotalItems(cart?: CartDetails | null): { count: number; lines?: num
return { count: sum, lines: cart.lines.length };
}

const findVariantBySelectedOptions = (
product: Product,
selectedOptions: Record<string, string>,
): Variant | undefined => {
const findVariantBySelectedOptions = (product: Product, selectedOptions: Options): Variant | undefined => {
return product?.variants.find((variant) =>
variant.selectedOptions.every((option) => selectedOptions[option.name] === option.value),
);
Expand Down Expand Up @@ -118,8 +117,11 @@ export const useCart = (product?: Product): CartContextType => {
});

const setSelectedVariant = useCallback(
(selectedOptions) => {
const variant = findVariantBySelectedOptions(product!, selectedOptions);
(selectedOptions: Options) => {
if (!product) {
return;
}
const variant = findVariantBySelectedOptions(product, selectedOptions);
if (variant) {
setSelectedVariantId(variant.id);
}
Expand All @@ -133,6 +135,10 @@ export const useCart = (product?: Product): CartContextType => {
console.error('Add to cart operation cannot be performed: cart is currently loading');
return;
}
if (!selectedVariantId) {
console.error('Add to cart operation cannot be performed: specify variantId');
return;
}

const lineItem = { merchandiseId: selectedVariantId, quantity };

Expand All @@ -145,12 +151,38 @@ export const useCart = (product?: Product): CartContextType => {
[selectedVariantId, cartId, initCartMutation, updateCartMutation, isLoading],
);

const addCustomVariantToCart = useCallback(
(quantity = 1, variantId = selectedVariantId) => {
if (isLoading) {
console.error('Add to cart operation cannot be performed: cart is currently loading');
return;
}
if (!variantId || quantity < 1) {
console.error('Add to cart operation cannot be performed: specify variantId and quantity');
return;
}

const lineItem = { merchandiseId: variantId, quantity };

if (cartId) {
updateCartMutation.mutate({ cartId, addLines: [lineItem] });
} else {
initCartMutation.mutate({ lines: [lineItem] });
}
},
[selectedVariantId, cartId, initCartMutation, updateCartMutation, isLoading],
);

const addOrUpdateCartItem = useCallback(
(quantity = 1) => {
if (isLoading) {
console.error('Update cart operation cannot be performed: cart is currently loading');
return;
}
if (!selectedVariantId) {
console.error('Update to cart operation cannot be performed: specify variantId');
return;
}

if (cartId) {
updateCartMutation.mutate({ cartId, addLines: [{ merchandiseId: selectedVariantId, quantity }] });
Expand All @@ -167,6 +199,10 @@ export const useCart = (product?: Product): CartContextType => {
console.error('Remove item operation cannot be performed: cart is currently loading');
return;
}
if (!cartId) {
console.error('Remove to cart operation cannot be performed: specify cartId');
return;
}

updateCartMutation.mutate({ cartId, removeLineIds: [lineId] });
},
Expand All @@ -179,6 +215,10 @@ export const useCart = (product?: Product): CartContextType => {
console.error('Change quantity operation cannot be performed: cart is currently loading');
return;
}
if (!cartId) {
console.error('Change quantity operation cannot be performed: specify cartId');
return;
}

updateCartMutation.mutate({ cartId, updateLines: [{ id: lineId, quantity }] });
},
Expand All @@ -195,6 +235,7 @@ export const useCart = (product?: Product): CartContextType => {
changeCartItemQuantity,
setSelectedVariant,
addSelectedVariantToCart,
addCustomVariantToCart,
isLoading,
};
};
Expand Down
1 change: 1 addition & 0 deletions apps/web/hooks/useProductRating/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useProductRating';
19 changes: 19 additions & 0 deletions apps/web/hooks/useProductRating/useProductRating.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Product } from '~/sdk/shopify/types';

const RATING = 5;
const RATING_COUNT = 5;

type ProductRating = {
rating: number;
ratingCount: number;
};

const useProductRating = (product: Product | null | undefined): ProductRating => {
if (!product) {
return { rating: 0, ratingCount: 1 };
}

return { rating: RATING, ratingCount: RATING_COUNT };
};

export default useProductRating;
Loading

0 comments on commit dca31a2

Please sign in to comment.