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

Feature/slider #6

Merged
merged 4 commits into from
Dec 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading