Skip to content

Commit

Permalink
Style selector basic functionality (#76)
Browse files Browse the repository at this point in the history
* Add prop-types package

* Create basic carousel component

* Remove unnecessary range function

* Clean up App

* Remove empty Carousel test file

* Set up data flow in Overview

* Add product details to ProductInformation

* Make ProductInformation props required

* Fix props for StyleSelector

* Use proper async useEffect pattern

* Select style by ID

Optional chaining is used to avoid errors resulting from lookups on
undefined.

* Add basic style selector functionality

* Add product details to ProductInformation

* Select style by ID

Optional chaining is used to avoid errors resulting from lookups on
undefined.

* Add basic style selector functionality

* Remove merge artifacts

* Test for style change on thumbnail click

* Move StyleSelector and AddToCart to Overview

* Simplify StyleSelector

Also makes changes for accessibility.
  • Loading branch information
slargman committed May 25, 2022
1 parent cfcc2c7 commit 0b0b80b
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 34 deletions.
23 changes: 14 additions & 9 deletions client/src/components/Overview/Overview.jsx
Expand Up @@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
import axios from 'axios';
import ImageGallery from 'Overview/ImageGallery.jsx';
import ProductInformation from 'Overview/ProductInformation.jsx';
import StyleSelector from 'Overview/StyleSelector.jsx';
import AddToCart from 'Overview/AddToCart.jsx';
import testData from 'tests/testData.js';

function Overview({ productId }) {
Expand All @@ -19,13 +21,15 @@ function Overview({ productId }) {
axios.get(`/products/${productId}/styles`),
]);

const sortedStyles = stylesResponse.data.results.sort((style1, style2) => (
style1.styled_id - style2.styled_id
));
const sortedStyles = stylesResponse.data.results.sort(
(style1, style2) => style1.styled_id - style2.styled_id
);

setProduct(productResponse.data);
setStyles(sortedStyles);
setSelectedStyleId(sortedStyles.find((style) => style['default?']).style_id);
setSelectedStyleId(
sortedStyles.find((style) => style['default?']).style_id
);
} catch (error) {
// TODO: handle error
}
Expand All @@ -40,18 +44,19 @@ function Overview({ productId }) {

return (
<div>
<ImageGallery
selectedStyleId={selectedStyleId}
/>
<ImageGallery selectedStyleId={selectedStyleId} />
<ProductInformation
// product={product}
product={testData.product}
selectedStyle={styles.find((style) => style.style_id === selectedStyleId)}
/>
<StyleSelector
// styles={styles}
selectedStyleId={selectedStyleId}
handleStyleSelect={handleStyleSelect}
// TODO: delete once fetching is working
product={testData.product}
styles={testData.styles.results}
/>
<AddToCart />
</div>
);
}
Expand Down
21 changes: 4 additions & 17 deletions client/src/components/Overview/ProductInformation.jsx
@@ -1,15 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import StyleSelector from 'Overview/StyleSelector.jsx';
import AddToCart from 'Overview/AddToCart.jsx';

/**
* Shows general product information
*/
function ProductInformation({ product, styles, selectedStyleId, handleStyleSelect }) {
// TODO: how to avoid optional chaining?
const selectedStyle = styles?.find((style) => style.style_id === selectedStyleId);

function ProductInformation({ product, selectedStyle}) {
return (
<div>
{/*TODO: add rating stars*/}
Expand All @@ -22,25 +17,17 @@ function ProductInformation({ product, styles, selectedStyleId, handleStyleSelec
Style &gt; {selectedStyle?.name}
<br />
{/*TODO: add strikethrough for sale*/}
{selectedStyle?.original_price}
{selectedStyle?.sale_price !== 0 ? selectedStyle?.sale_price : null}
{/*TODO: show Product Overview*/}
{/*TODO: add Share buttons*/}
<StyleSelector
styles={styles}
selectedStyleId={selectedStyleId}
handleStyleSelect={handleStyleSelect}
/>
<AddToCart />
{selectedStyle?.original_price}
{selectedStyle?.sale_price !== 0 ? selectedStyle?.sale_price : null}
</div>
);
}

ProductInformation.propTypes = {
product: PropTypes.object.isRequired,
styles: PropTypes.array.isRequired,
selectedStyleId: PropTypes.number.isRequired,
handleStyleSelect: PropTypes.func.isRequired,
selectedStyle: PropTypes.object.isRequired,
};

export default ProductInformation;
10 changes: 4 additions & 6 deletions client/src/components/Overview/ProductInformation.test.jsx
Expand Up @@ -3,21 +3,19 @@ import ProductInformation from 'Overview/ProductInformation.jsx';

describe('ProductInformation', () => {
it('should show the price for the selected style', () => {
const styles = testData.styles.results;

const { rerender } = render(<ProductInformation
product={testData.product}
styles={testData.styles.results}
selectedStyleId={1}
handleStyleSelect={() => {}}
selectedStyle={styles.find((style) => style.style_id === 1)}
/>);

let node = screen.getByText(/(?<=Style > )(.+?)$/);
expect(node).toHaveTextContent('Forest Green & Black');

rerender(<ProductInformation
product={testData.product}
styles={testData.styles.results}
selectedStyleId={2}
handleStyleSelect={() => {}}
selectedStyle={styles.find((style) => style.style_id === 2)}
/>);

node = screen.getByText(/(?<=Style > )(.+?)$/);
Expand Down
51 changes: 49 additions & 2 deletions client/src/components/Overview/StyleSelector.jsx
@@ -1,7 +1,54 @@
import React from 'react';
import PropTypes from 'prop-types';

function StyleSelector() {
return null;
/**
* Displays thumbnails of the product styles and
* allows style selection by clicking on a thumbnail
*/
function Style({ style, handleStyleSelect, selected }) {
// TODO: handle selected overlay
// TODO:
return (
<button
type="button"
onClick={() => handleStyleSelect(style.style_id)}
>
<img
src={style.photos[0].thumbnail_url}
alt={`${style.name} style thumbnail`}
width="50"
/>
</button>
);
}

Style.propTypes = {
style: PropTypes.object.isRequired,
handleStyleSelect: PropTypes.func.isRequired,
selected: PropTypes.bool.isRequired,
};

function StyleSelector({ styles, selectedStyleId, handleStyleSelect }) {
// TODO: use color-thief to extract color of thumbnails server-side
// or parse style name
return (
<div>
{styles.map((style) => (
<Style
key={style.style_id}
style={style}
handleStyleSelect={handleStyleSelect}
selected={style.style_id === selectedStyleId}
/>
))}
</div>
);
}

StyleSelector.propTypes = {
styles: PropTypes.array.isRequired,
selectedStyleId: PropTypes.number.isRequired,
handleStyleSelect: PropTypes.func.isRequired,
};

export default StyleSelector;
33 changes: 33 additions & 0 deletions client/src/components/Overview/StyleSelector.test.jsx
@@ -0,0 +1,33 @@
import { screen } from '@testing-library/react';
import StyleSelector from 'Overview/StyleSelector.jsx';

describe('StyleSelector', () => {
it('should change selected style when a thumbnail is clicked', async () => {
const user = userEvent.setup();
const handleStyleSelect = (styleId) => {
selectedStyleId = styleId;
};
let selectedStyleId = 1;

const {rerender} = render(<StyleSelector
styles={testData.styles.results}
selectedStyleId={selectedStyleId}
handleStyleSelect={handleStyleSelect}
/>);

//TODO: might need to be more specific as thumbnails get added to imageGallery
const style = 'Desert Brown & Tan';
await user.click(screen.getByAltText(new RegExp(style)));

const selectedStyleObj = Object.values(testData.styles.results)
.find(styleObj => styleObj.style_id === selectedStyleId);
const selectedStyle = selectedStyleObj.name;

expect(selectedStyle).toBe(style);
});

it.todo('should show all styles for a product');
it.todo('should initially select the default style <- should go in Overview')
it.todo('should not change style when the currently selected style thumbnail is clicked');
it.todo('should indicate the selected style with an overlay');
});

0 comments on commit 0b0b80b

Please sign in to comment.