Skip to content

Commit

Permalink
Feature/align products (#3)
Browse files Browse the repository at this point in the history
* Update Name

* update getProducts

* Align with SDK

* Update Product details
  • Loading branch information
usulpro authored Dec 27, 2023
1 parent 2011a4a commit 9635f90
Show file tree
Hide file tree
Showing 12 changed files with 235 additions and 154 deletions.
4 changes: 4 additions & 0 deletions apps/web/components/Gallery/Gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export function Gallery({ images, className, ...attributes }: GalleryProps) {
return null;
};

if (!images.map) {
return null;
}

const onChangeIndex = (index: number) => {
cancel();
setActiveIndex(() => clamp(index, 0, images.length - 1));
Expand Down
90 changes: 48 additions & 42 deletions apps/web/components/ProductAccordion/ProductAccordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,49 +20,55 @@ export function ProductAccordion({ product, ...attributes }: ProductAccordionPro

return (
<div {...attributes}>
<SfAccordionItem
summary={
<>
<h2 className="font-bold font-headings text-lg leading-6 md:text-2xl">{t('productDetails')}</h2>
<SfIconExpandMore
className={classNames('text-neutral-500', {
'rotate-180': isOpen('description'),
})}
/>
</>
}
summaryClassName="md:rounded-md w-full hover:bg-neutral-100 py-2 pl-4 pr-3 flex justify-between items-center"
open={isOpen('description')}
onToggle={handleToggle('description')}
>
<div className="py-2">
<p className="text-neutral-900 px-4">{description}</p>
</div>
</SfAccordionItem>
<Divider className="my-4" />
<SfAccordionItem
summary={
<>
<h2 className="font-bold font-headings text-lg leading-6 md:text-2xl">{t('customerReviews')}</h2>
<SfIconExpandMore
className={classNames('text-neutral-500', {
'rotate-180': isOpen('reviews'),
})}
/>
</>
}
summaryClassName="md:rounded-md w-full hover:bg-neutral-100 py-2 pl-4 pr-3 flex justify-between items-center"
open={isOpen('reviews')}
onToggle={handleToggle('reviews')}
>
<div className="py-2">
<div className="text-neutral-900 px-4">
{reviews.map((review) => (
<Review review={review} key={review.id} />
))}
{description ? (
<>
<SfAccordionItem
summary={
<>
<h2 className="font-bold font-headings text-lg leading-6 md:text-2xl">{t('productDetails')}</h2>
<SfIconExpandMore
className={classNames('text-neutral-500', {
'rotate-180': isOpen('description'),
})}
/>
</>
}
summaryClassName="md:rounded-md w-full hover:bg-neutral-100 py-2 pl-4 pr-3 flex justify-between items-center"
open={isOpen('description')}
onToggle={handleToggle('description')}
>
<div className="py-2">
<p className="text-neutral-900 px-4">{description}</p>
</div>
</SfAccordionItem>
<Divider className="my-4" />
</>
) : null}
{reviews.length > 0 ? (
<SfAccordionItem
summary={
<>
<h2 className="font-bold font-headings text-lg leading-6 md:text-2xl">{t('customerReviews')}</h2>
<SfIconExpandMore
className={classNames('text-neutral-500', {
'rotate-180': isOpen('reviews'),
})}
/>
</>
}
summaryClassName="md:rounded-md w-full hover:bg-neutral-100 py-2 pl-4 pr-3 flex justify-between items-center"
open={isOpen('reviews')}
onToggle={handleToggle('reviews')}
>
<div className="py-2">
<div className="text-neutral-900 px-4">
{reviews.map((review) => (
<Review review={review} key={review.id} />
))}
</div>
</div>
</div>
</SfAccordionItem>
</SfAccordionItem>
) : null}
</div>
);
}
91 changes: 45 additions & 46 deletions apps/web/components/ProductProperties/ProductProperties.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,57 @@
import { SfChip, SfThumbnail } from '@storefront-ui/react';
import { useTranslation } from 'next-i18next';
import type { ProductPropertiesProps } from '~/components';
import { Divider, type ProductPropertiesProps } from '~/components';
import { useProductAttribute } from '~/hooks';

export function ProductProperties({ product, showColors = true, ...attributes }: ProductPropertiesProps): JSX.Element {
const { t } = useTranslation();

const { getAttributeList, getAttribute, setAttribute } = useProductAttribute(product, ['Color', 'Size']);
const { getOptions, getAttributeList, getAttribute, setAttribute } = useProductAttribute(product, ['Color', 'Size']);
const options = getOptions();

const sizes = getAttributeList('Size');
const colors = getAttributeList('Color');
const selectedSize = getAttribute('Size');
const selectedColor = getAttribute('Color');
if (options.length === 0) {
return null;
}

return (
<div className="px-4" {...attributes}>
{sizes.length > 1 && (
<>
<span className="block mb-2 mt-2 text-base font-medium leading-6 text-neutral-900">{t('size')}</span>
{sizes.map(({ label, value }) => (
<div className="mr-2 mb-2 uppercase inline-block" key={value}>
<SfChip
className="min-w-[48px]"
size="sm"
inputProps={{
checked: value === selectedSize,
onChange: () => setAttribute('Size', value),
}}
>
{label}
</SfChip>
</div>
))}
</>
)}
{colors.length > 1 && (
<>
<span className="block mb-2 mt-2 text-base font-medium leading-6 text-neutral-900">{t('color')}</span>
{colors.map(({ value, label }) => (
<div key={value} className="mr-2 mb-2 inline-block">
<SfChip
slotPrefix={showColors ? <SfThumbnail size="sm" style={{ background: value }} /> : null}
size="sm"
inputProps={{
checked: value === selectedColor,
onChange: () => setAttribute('Color', value),
}}
>
{label}
</SfChip>
</div>
))}
</>
)}
</div>
<>
<Divider className="mb-6" />
<div className="px-4" {...attributes}>
{options.map((option) => {
const optionValues = getAttributeList(option);
const selectedValue = getAttribute(option);

return (
optionValues.length > 1 && (
<div key={option}>
<span className="block mb-2 mt-2 text-base font-medium leading-6 text-neutral-900">
{t(option.toLowerCase())}
</span>
{optionValues.map(({ label, value }) => (
<div className="mr-2 mb-2 uppercase inline-block" key={value}>
<SfChip
className="min-w-[48px]"
size="sm"
slotPrefix={
option === 'Color' && showColors ? (
<SfThumbnail size="sm" style={{ background: value }} />
) : null
}
inputProps={{
checked: value === selectedValue,
onChange: () => setAttribute(option, value),
}}
>
{label}
</SfChip>
</div>
))}
</div>
)
);
})}
</div>
<Divider className="mt-4 mb-2 md:mt-8" />
</>
);
}
5 changes: 3 additions & 2 deletions apps/web/components/ProductSlider/ProductSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ const useProducts = () => {
const [products, setProducts] = React.useState([]);

const getData = async () => {
const resp = await sdk.shopify.getProducts();
setProducts(processProducts(resp));
const resp = await sdk.shopify.getProducts({});
console.log('🚀 ~ file: ProductSlider.tsx:26 ~ getData ~ resp:', resp);
setProducts(resp.products);
};

React.useEffect(() => {
Expand Down
20 changes: 11 additions & 9 deletions apps/web/components/ui/ProductCard/ProductCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,17 @@ export function ProductCard({
>
<div className="relative">
<SfLink href={`/product/${slug}`} as={Link} className="relative block w-full pb-[100%]">
<Image
src={imageUrl ?? ''}
alt={imageAlt || 'primary image'}
className="object-cover rounded-md aspect-square w-full h-full"
data-testid="image-slot"
fill
sizes="(max-width: 768px) 50vw, 190px"
priority={priority}
/>
{imageUrl && (
<Image
src={imageUrl ?? ''}
alt={imageAlt || 'primary image'}
className="object-cover rounded-md aspect-square w-full h-full"
data-testid="image-slot"
fill
sizes="(max-width: 768px) 50vw, 190px"
priority={priority}
/>
)}
</SfLink>
</div>
<div
Expand Down
7 changes: 3 additions & 4 deletions apps/web/hooks/useProduct/useProduct.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { QueryClient, useQuery } from '@tanstack/react-query';
import { SfProduct } from '@vue-storefront/unified-data-model';
import { sdk } from '~/sdk';
import { Product } from '~/sdk/shopify/types';

const fetchProduct = async (slug: string): Promise<SfProduct> => {
const data = await sdk.shopify.getProduct({ handle: slug });
return data;
const fetchProduct = async (slug: string): Promise<Product> => {
return await sdk.shopify.getProduct({ slug });
};

export async function prefetchProduct(slug: string): Promise<QueryClient> {
Expand Down
86 changes: 43 additions & 43 deletions apps/web/hooks/useProductAttribute/useProductAttribute.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
import { useState } from 'react';
import { SfAttribute, SfProduct } from '@vue-storefront/unified-data-model';
import { get, map, defaults as withDefaults, zipObject, groupBy, uniqBy, pick, mapValues } from 'lodash-es';

/**
* Hook for getting product attributes data
* @param {SfProduct} product Product object
*/
export function useProductAttribute<TAttribute extends string>(product: SfProduct, attributesNames: TAttribute[] = []) {
// const attributes = groupBy(
// uniqBy(
// (product?.variants || []).flatMap((variant) => variant?.attributes),
// 'value',
// ),
// 'name',
// );
// const mapAttribute = (attributes: SfAttribute[] = []) => {
// const mappedAttributes = mapValues(
// pick(groupBy(attributes, 'name'), attributesNames),
// (attribute) => attribute[0].value,
// );

// const defaults = zipObject(
// attributesNames,
// map(attributesNames, () => null),
// );
// return withDefaults(mappedAttributes, defaults);
// };
const defaultAttributes = Object.entries(product.attributes)
.map(([name, values]) => ({ [name]: values[0].value }))
.reduce((obj, attr) => ({ ...obj, ...attr }), {});

const [selectedAttrs, setSelectedAttrs] = useState(defaultAttributes);

return {
getAttributeList: (attributeName: TAttribute) => get(product.attributes, attributeName, [] as SfAttribute[]),
getAttribute: (attributeName: TAttribute) => get(selectedAttrs, attributeName, null),
setAttribute: (attributeName: TAttribute, attributeValue: string) => {
setSelectedAttrs((previous) => ({
...previous,
[attributeName]: attributeValue,
}));
},
};
import { Product } from '~/sdk/shopify/types';

interface AttributeValue {
label: string;
value: string;
}

interface UseProductAttribute {
getAttributeList: (attributeName: string) => AttributeValue[];
getAttribute: (attributeName: string) => string;
setAttribute: (attributeName: string, value: string) => void;
getOptions: () => string[];
}

export const useProductAttribute = (product: Product, attributes: string[]): UseProductAttribute => {
const initialAttributes = attributes.reduce<Record<string, string>>((accumulator, attribute) => {
const option = product.options.find((opt) => opt.name === attribute);
if (option && option.values.length > 0) {
accumulator[attribute] = option.values[0];
}
return accumulator;
}, {});

const [selectedAttributes, setSelectedAttributes] = useState<Record<string, string>>(initialAttributes);

const getAttributeList = (attributeName: string): AttributeValue[] => {
const attribute = product.options.find((option) => option.name === attributeName);
return attribute ? attribute.values.map((value) => ({ label: value, value })) : [];
};

const getAttribute = (attributeName: string): string => {
return selectedAttributes[attributeName] || '';
};

const setAttribute = (attributeName: string, value: string): void => {
setSelectedAttributes((previous) => ({ ...previous, [attributeName]: value }));
};

const getOptions = (): string[] => {
return product.options.map((option) => option.name);
};

return { getAttributeList, getAttribute, setAttribute, getOptions };
};
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "web",
"name": "shopify-storefront",
"version": "0.1.0",
"private": true,
"main": "",
Expand Down
Loading

0 comments on commit 9635f90

Please sign in to comment.