From 2b244eeafe837d3e7d08b98232d145160753cb1c Mon Sep 17 00:00:00 2001 From: OlaTobiszewska Date: Mon, 4 May 2020 16:19:59 +0200 Subject: [PATCH 1/2] Add CartIcon component, Wishlist in progress --- src/App.scss | 4 + src/App.tsx | 11 ++ src/common/mocks/wishlistProducts.ts | 17 +++ src/components/atoms/Text/Text.tsx | 36 +++--- .../molecules/CartIcon/CartIcon.spec.js | 67 +++++++++++ .../molecules/CartIcon/CartIcon.tsx | 73 ++++++++++++ .../__snapshots__/CartIcon.spec.js.snap | 39 +++++++ .../organisms/Wishlist/Wishlist.spec.js | 104 ++++++++++++++++++ .../organisms/Wishlist/Wishlist.tsx | 26 ++++- .../organisms/Wishlist/WishlistItem.tsx | 80 ++++++++++++++ .../organisms/Wishlist/wishlist.scss | 22 ++++ src/stories/CartIcon.stories.jsx | 26 +++++ src/stories/styles.scss | 6 + 13 files changed, 494 insertions(+), 17 deletions(-) create mode 100644 src/common/mocks/wishlistProducts.ts create mode 100644 src/components/molecules/CartIcon/CartIcon.spec.js create mode 100644 src/components/molecules/CartIcon/CartIcon.tsx create mode 100644 src/components/molecules/CartIcon/__snapshots__/CartIcon.spec.js.snap create mode 100644 src/components/organisms/Wishlist/Wishlist.spec.js create mode 100644 src/components/organisms/Wishlist/WishlistItem.tsx create mode 100644 src/components/organisms/Wishlist/wishlist.scss create mode 100644 src/stories/CartIcon.stories.jsx diff --git a/src/App.scss b/src/App.scss index 9521d92..03b2fb5 100644 --- a/src/App.scss +++ b/src/App.scss @@ -10,3 +10,7 @@ .firstSelect { z-index: 1; } + +.wishlist { + width: 50%; +} diff --git a/src/App.tsx b/src/App.tsx index c57797c..dddb290 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,6 +19,9 @@ import './components/atoms/Breadcrumbs/Breadcrumbs.scss'; import Toast from './components/atoms/Toast/Toast'; import Select from './components/atoms/Select/Select'; import selectItems from './common/mocks/selectItems'; +import Wishlist from './components/organisms/Wishlist/Wishlist'; +import wishlistProducts from './common/mocks/wishlistProducts'; +import CartIcon from './components/molecules/CartIcon/CartIcon'; function App() { const [value, setValue] = useState(''); @@ -130,6 +133,14 @@ function App() { setMultipleSelectOption(option); }} /> + console.log('item clicked!')} + onDeleteIconClick={() => { + console.log('delete!'); + }} + /> ); } diff --git a/src/common/mocks/wishlistProducts.ts b/src/common/mocks/wishlistProducts.ts new file mode 100644 index 0000000..84d738c --- /dev/null +++ b/src/common/mocks/wishlistProducts.ts @@ -0,0 +1,17 @@ +export default [ + { + name: 'Blue summer hat', + price: '25.00 EUR', + image: 'https://cdn.pixabay.com/photo/2015/10/13/03/39/fashion-985556_1280.jpg', + }, + { + name: 'Huge sombrero', + price: '30.99 EUR', + image: 'https://cdn.pixabay.com/photo/2015/12/08/01/04/sombrero-1082322_1280.jpg', + }, + { + name: 'Hunter hat', + price: '23.50 EUR', + image: 'https://cdn.pixabay.com/photo/2016/11/18/14/15/forest-1834831_1280.jpg', + } +] diff --git a/src/components/atoms/Text/Text.tsx b/src/components/atoms/Text/Text.tsx index 160ff22..5b9d044 100644 --- a/src/components/atoms/Text/Text.tsx +++ b/src/components/atoms/Text/Text.tsx @@ -1,24 +1,30 @@ import React from 'react'; import styled from 'styled-components'; -import { COLOR_OPTIONS, TEXT_ALIGNMENT_OPTIONS, FONT_WEIGHT_OPTIONS } from '../../../common/constants/consts'; +import { + COLOR_OPTIONS, + TEXT_ALIGNMENT_OPTIONS, + FONT_WEIGHT_OPTIONS, +} from '../../../common/constants/consts'; -interface IText { +export type FontWeights = + | 'normal' + | 'bold' + | '100' + | '200' + | '300' + | '400' + | '500' + | '600' + | '700' + | '800' + | '900'; + +export interface IText { className?: string; - children: string; + children?: string; color?: string; fontColor?: string; - fontWeight?: - | 'normal' - | 'bold' - | '100' - | '200' - | '300' - | '400' - | '500' - | '600' - | '700' - | '800' - | '900'; + fontWeight?: FontWeights; lineHeight?: number; fontSize?: number; textAlign?: 'left' | 'right' | 'center' | 'justify'; diff --git a/src/components/molecules/CartIcon/CartIcon.spec.js b/src/components/molecules/CartIcon/CartIcon.spec.js new file mode 100644 index 0000000..ebd7eae --- /dev/null +++ b/src/components/molecules/CartIcon/CartIcon.spec.js @@ -0,0 +1,67 @@ +import React from 'react'; +import { create, act } from 'react-test-renderer'; +import 'jest-styled-components'; +import { ThemeProvider } from 'styled-components'; +import theme from '../../../common/theme'; +import CartIcon from './CartIcon'; +import Badge from '../../atoms/Badge/Badge'; + +describe('Cart Icon component', () => { + it('should render correctly', () => { + const cartIcon = create( + + + + ).toJSON(); + expect(cartIcon).toMatchSnapshot(); + }) + + it('should render icon in default theme color and default size', () => { + const cartIcon = create( + + + + ).toJSON(); + expect(cartIcon).toHaveStyleRule('background-color', '#000000'); + expect(cartIcon).toHaveStyleRule('color', '#ffffff'); + expect(cartIcon).toHaveStyleRule('width', '40px'); + }); + + it('should render icon in custom colors', () => { + const cartIcon = create( + + + + ).toJSON(); + expect(cartIcon).toHaveStyleRule('background-color', 'red'); + expect(cartIcon).toHaveStyleRule('color', 'blue'); + expect(cartIcon).toHaveStyleRule('width', '30px'); + }) + + it('should render with product number label', () => { + const cartIcon = create( + + + + ) + const instance = cartIcon.root; + const badge = instance.findByType(Badge).findByType('span'); + expect(badge.props.children).toBe(3); + }) + + it('should call onClick method', () => { + const mockFn = jest.fn(); + const e = { stopPropagation: jest.fn() }; + const cartIcon = create( + + + + ) + const instance = cartIcon.root; + const icon = instance.findByType('svg'); + act(() => { + icon.props.onClick(e); + }) + expect(mockFn.mock.calls.length).toEqual(1); + }) +}); diff --git a/src/components/molecules/CartIcon/CartIcon.tsx b/src/components/molecules/CartIcon/CartIcon.tsx new file mode 100644 index 0000000..f18a7d6 --- /dev/null +++ b/src/components/molecules/CartIcon/CartIcon.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import styled from 'styled-components'; +import { CartPlus } from '@styled-icons/fa-solid/CartPlus'; +import Badge from '../../atoms/Badge/Badge'; +import { COLOR_OPTIONS } from '../../../common/constants/consts'; + +interface ICartIcon { + color?: string; + isActive?: boolean; + backgroundColor?: string; + fontColor?: string; + width?: number; +} + +interface ICartIconContainer extends ICartIcon { + className?: string; + onCartIconClick?: () => void; + hideCartNumberLabel?: boolean; + numberOfItemsInCart?: number; +} + +export const CartIcon = styled(CartPlus)` + ${({ theme, color = COLOR_OPTIONS.primary, isActive, fontColor, backgroundColor, width = 40 }) => ({ + color: fontColor || theme.colors[color].light, + 'background-color': backgroundColor || theme.colors[color].base, + width: `${width}px`, + padding: '7px', + 'border-radius': '5px', + transition: 'transform .2s', + transform: isActive ? 'scale(1.2)' : 'scale(1)', + cursor: 'pointer', + })} +`; + +export default ({ + color, + isActive = false, + onCartIconClick, + hideCartNumberLabel, + numberOfItemsInCart = 0, + className, + fontColor, + backgroundColor, + width +}: ICartIconContainer) => { + const handleOnCartIconClick = (e: React.MouseEvent) => { + e.stopPropagation(); + onCartIconClick && onCartIconClick(); + }; + + if (hideCartNumberLabel || numberOfItemsInCart === 0) { + return ( +
+ +
+ ); + } + return ( +
+ + + +
+ ); +}; diff --git a/src/components/molecules/CartIcon/__snapshots__/CartIcon.spec.js.snap b/src/components/molecules/CartIcon/__snapshots__/CartIcon.spec.js.snap new file mode 100644 index 0000000..35ba987 --- /dev/null +++ b/src/components/molecules/CartIcon/__snapshots__/CartIcon.spec.js.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Cart Icon component should render correctly 1`] = ` +.c0 { + display: inline-block; + vertical-align: -.125em; + overflow: hidden; +} + +.c1 { + color: #ffffff; + background-color: #000000; + width: 40px; + padding: 7px; + border-radius: 5px; + -webkit-transition: -webkit-transform .2s; + -webkit-transition: transform .2s; + transition: transform .2s; + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + cursor: pointer; +} + + +`; diff --git a/src/components/organisms/Wishlist/Wishlist.spec.js b/src/components/organisms/Wishlist/Wishlist.spec.js new file mode 100644 index 0000000..55ea9fe --- /dev/null +++ b/src/components/organisms/Wishlist/Wishlist.spec.js @@ -0,0 +1,104 @@ +import React from 'react'; +import { create, act } from 'react-test-renderer'; +import { render } from '@testing-library/react'; +import 'jest-styled-components'; +import { ThemeProvider } from 'styled-components'; +import theme from '../../../common/theme'; +import Wishlist from './Wishlist'; +import WishlistItem, {StyledWishlistDeleteIcon} from './WishlistItem'; +import wishlistProducts from '../../../common/mocks/wishlistProducts'; +import setupIntersectionObserverMock from '../../../helpers/intersectionObserverMock'; +import Text from '../../atoms/Text/Text'; + +describe('Wishlist component', () => { + beforeEach(() => { + setupIntersectionObserverMock(); + }); + + it('should render wishlist item with proper product data', () => { + const item = create( + + + + ); + const instance = item.root; + const info = instance.findByType(WishlistItem).findAllByType(Text); + expect(info[0].props.children).toBe('Blue summer hat'); + expect(info[1].props.children).toBe('25.00 EUR'); + }); + + it('should render wishlist item text in color based on color prop', () => { + const { getByText } = render( + + + + ); + const text = getByText('Blue summer hat'); + expect(text).toHaveStyle('color: #2196f3'); + }); + + it('should render wishlist item name and price with custom props', () => { + const { getByText } = render( + + + + ); + const name = getByText('Blue summer hat'); + const price = getByText('25.00 EUR'); + expect(name).toHaveStyle('color: red'); + expect(name).toHaveStyle('font-weight: 800'); + expect(price).toHaveStyle('font-size: 12px'); + expect(price).toHaveStyle('text-align: center'); + }); + + it('should call onClick function', () => { + const mockFn = jest.fn(); + const wishlist = create( + + + + ) + const instance = wishlist.root; + const item = instance.findAllByType(WishlistItem)[0].findByType('div'); + act(() => { + item.props.onClick(); + }) + expect(mockFn.mock.calls.length).toEqual(1); + }) + + it('should call onDeleteIconClick function', () => { + const mockFn = jest.fn(); + const e = { stopPropagation: jest.fn() }; + const wishlist = create( + + + + ) + const instance = wishlist.root; + const deleteIcon = instance.findAllByType(StyledWishlistDeleteIcon)[0]; + act(() => { + deleteIcon.props.onClick(e); + }) + expect(mockFn.mock.calls.length).toEqual(1); + }) + + it('should call onCartIconClick function', () => { + const mockFn = jest.fn(); + const e = { stopPropagation: jest.fn() }; + const wishlist = create( + + + + ) + const instance = wishlist.root; + const cartIcon = instance.findAllByType(CartIcon)[0]; + act(() => { + cartIcon.props.onClick(e); + }) + expect(mockFn.mock.calls.length).toEqual(1); + }) +}); diff --git a/src/components/organisms/Wishlist/Wishlist.tsx b/src/components/organisms/Wishlist/Wishlist.tsx index 523e337..63ef986 100644 --- a/src/components/organisms/Wishlist/Wishlist.tsx +++ b/src/components/organisms/Wishlist/Wishlist.tsx @@ -1,6 +1,28 @@ import React from 'react'; +import WishlistItem, { IWishlistCommon, IWishlistProduct } from './WishlistItem'; -export const Wishlist: React.StatelessComponent<{}> = () => ( -
Wishlist +interface IWishlist extends IWishlistCommon { + className?: string; + products: IWishlistProduct[]; +} + +const Wishlist = ({ className, products, color, nameTextProps, priceTextProps, itemClassName, onDeleteIconClick, onItemClick, deleteIconColor, hideDeleteIcon }: IWishlist) => ( +
+ {products.map((product, index) => ( + + ))}
); + +export default Wishlist; diff --git a/src/components/organisms/Wishlist/WishlistItem.tsx b/src/components/organisms/Wishlist/WishlistItem.tsx new file mode 100644 index 0000000..b1cc635 --- /dev/null +++ b/src/components/organisms/Wishlist/WishlistItem.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import styled from 'styled-components'; +import Text, {FontWeights, IText} from '../../atoms/Text/Text'; +import Image from '../../atoms/Image/Image'; +import './wishlist.scss'; +import { FONT_WEIGHT_OPTIONS } from '../../../common/constants/consts'; +import { DeleteForever } from '@styled-icons/material-sharp/DeleteForever'; + +export interface IWishlistProduct { + name: string; + price: string; + image: string; +} + +export interface IWishlistCommon { + itemClassName?: string; + hideHeartDislike?: boolean; + hideCartIcon?: boolean; + color?: string; + onClick?: () => void; + nameTextProps?: IText; + priceTextProps?: IText; + onDeleteIconClick?: () => void; + onItemClick?: () => void; + deleteIconColor?: string; + hideDeleteIcon?: boolean; +} + +interface IWishlistItem extends IWishlistCommon { + product: IWishlistProduct; +} + +export const StyledWishlistDeleteIcon = styled(DeleteForever)<{ color?: string, deleteIconColor?: string }>`${({ + theme, color = defaultProps.color, deleteIconColor +}) => ({ + color: deleteIconColor || theme.colors[color].base, + width: '30px', + position: 'absolute', + bottom: '0', + right: '0', + cursor: 'pointer', +})}` + +const WishlistItem = ({ product, color, nameTextProps, priceTextProps, itemClassName, onDeleteIconClick, onItemClick, deleteIconColor, hideDeleteIcon }: IWishlistItem) => { + const nameProps = { + color, + fontWeight: FONT_WEIGHT_OPTIONS['600'] as FontWeights, + ...nameTextProps, + }; + const priceProps = { + color, + ...priceTextProps, + }; + return ( +
+
+ {product.name} +
+
+ {product.name} + {product.price} + { !hideDeleteIcon && ( + { + e.stopPropagation(); + onDeleteIconClick && onDeleteIconClick(); + }} /> + )} +
+
+ ); +}; + +const defaultProps = { + color: 'primary', + hideDeleteIcon: false, +} + +WishlistItem.defaultProps = defaultProps; + +export default WishlistItem; diff --git a/src/components/organisms/Wishlist/wishlist.scss b/src/components/organisms/Wishlist/wishlist.scss new file mode 100644 index 0000000..3fc193e --- /dev/null +++ b/src/components/organisms/Wishlist/wishlist.scss @@ -0,0 +1,22 @@ +.wishlistItem { + height: 100px; + display: flex; + padding: 10px; + &:hover { + box-shadow: 0px 4px 10px 4px rgba(217,217,217,0.6); + } +} + +.wishlistItem__imageWrapper { + height: 100%; + width: 100px; + margin-right: 10px; +} + +.wishlistItem__textWrapper { + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + position: relative; +} diff --git a/src/stories/CartIcon.stories.jsx b/src/stories/CartIcon.stories.jsx new file mode 100644 index 0000000..6bd6323 --- /dev/null +++ b/src/stories/CartIcon.stories.jsx @@ -0,0 +1,26 @@ +import React, { useState } from 'react'; +import { storiesOf } from '@storybook/react'; +import { withKnobs, select, boolean, number } from '@storybook/addon-knobs'; +import CartIcon from '../components/molecules/CartIcon/CartIcon'; +import { COLOR_OPTIONS } from '../common/constants/consts'; +import './styles.scss'; + +const stories = storiesOf('Cart icon', module); +stories.addDecorator(withKnobs); + +stories.add('common', () => { + const [productCount, setProductCount] = useState(0); + + return ( + { + setProductCount(productCount + 1); + }} + color={select('Color', COLOR_OPTIONS, COLOR_OPTIONS.primary)} + width={number('Width', 40)} + hideCartNumberLabel={boolean('Hide label', false)} + /> + ); +}); diff --git a/src/stories/styles.scss b/src/stories/styles.scss index c84f20f..3b257ec 100644 --- a/src/stories/styles.scss +++ b/src/stories/styles.scss @@ -40,3 +40,9 @@ .drawerButton { width: 100px; } + +.cartIcon { + position: absolute; + top: 50px; + left: 50px; +} From a01e6e8b09ce90884267d183024b85c011856b99 Mon Sep 17 00:00:00 2001 From: OlaTobiszewska Date: Tue, 5 May 2020 17:08:43 +0200 Subject: [PATCH 2/2] add story for wishlist, use CartIcon component --- src/App.tsx | 11 - src/common/mocks/wishlistProducts.ts | 6 + .../molecules/CartIcon/CartIcon.spec.js | 6 +- .../molecules/CartIcon/CartIcon.tsx | 104 +++++-- .../__snapshots__/CartIcon.spec.js.snap | 47 ++- .../molecules/CartIcon/cartIcon.scss | 14 + .../organisms/Wishlist/Wishlist.spec.js | 27 +- .../organisms/Wishlist/Wishlist.tsx | 4 +- .../organisms/Wishlist/WishlistItem.tsx | 47 ++- .../__snapshots__/Wishlist.spec.js.snap | 276 ++++++++++++++++++ .../organisms/Wishlist/wishlist.scss | 10 +- src/stories/CartIcon.stories.jsx | 1 + src/stories/Wishlist.stories.jsx | 77 +++++ src/stories/styles.scss | 7 + 14 files changed, 558 insertions(+), 79 deletions(-) create mode 100644 src/components/molecules/CartIcon/cartIcon.scss create mode 100644 src/components/organisms/Wishlist/__snapshots__/Wishlist.spec.js.snap create mode 100644 src/stories/Wishlist.stories.jsx diff --git a/src/App.tsx b/src/App.tsx index dddb290..c57797c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,9 +19,6 @@ import './components/atoms/Breadcrumbs/Breadcrumbs.scss'; import Toast from './components/atoms/Toast/Toast'; import Select from './components/atoms/Select/Select'; import selectItems from './common/mocks/selectItems'; -import Wishlist from './components/organisms/Wishlist/Wishlist'; -import wishlistProducts from './common/mocks/wishlistProducts'; -import CartIcon from './components/molecules/CartIcon/CartIcon'; function App() { const [value, setValue] = useState(''); @@ -133,14 +130,6 @@ function App() { setMultipleSelectOption(option); }} /> - console.log('item clicked!')} - onDeleteIconClick={() => { - console.log('delete!'); - }} - /> ); } diff --git a/src/common/mocks/wishlistProducts.ts b/src/common/mocks/wishlistProducts.ts index 84d738c..2b4a18e 100644 --- a/src/common/mocks/wishlistProducts.ts +++ b/src/common/mocks/wishlistProducts.ts @@ -3,15 +3,21 @@ export default [ name: 'Blue summer hat', price: '25.00 EUR', image: 'https://cdn.pixabay.com/photo/2015/10/13/03/39/fashion-985556_1280.jpg', + id: '1', + // inCartCount: 0, }, { name: 'Huge sombrero', price: '30.99 EUR', image: 'https://cdn.pixabay.com/photo/2015/12/08/01/04/sombrero-1082322_1280.jpg', + id: '2', + // inCartCount: 0, }, { name: 'Hunter hat', price: '23.50 EUR', image: 'https://cdn.pixabay.com/photo/2016/11/18/14/15/forest-1834831_1280.jpg', + id: '3', + inCartCount: 0, } ] diff --git a/src/components/molecules/CartIcon/CartIcon.spec.js b/src/components/molecules/CartIcon/CartIcon.spec.js index ebd7eae..181fb88 100644 --- a/src/components/molecules/CartIcon/CartIcon.spec.js +++ b/src/components/molecules/CartIcon/CartIcon.spec.js @@ -17,22 +17,24 @@ describe('Cart Icon component', () => { }) it('should render icon in default theme color and default size', () => { - const cartIcon = create( + const tree = create( ).toJSON(); + const cartIcon = tree.children[0]; expect(cartIcon).toHaveStyleRule('background-color', '#000000'); expect(cartIcon).toHaveStyleRule('color', '#ffffff'); expect(cartIcon).toHaveStyleRule('width', '40px'); }); it('should render icon in custom colors', () => { - const cartIcon = create( + const tree = create( ).toJSON(); + const cartIcon = tree.children[0]; expect(cartIcon).toHaveStyleRule('background-color', 'red'); expect(cartIcon).toHaveStyleRule('color', 'blue'); expect(cartIcon).toHaveStyleRule('width', '30px'); diff --git a/src/components/molecules/CartIcon/CartIcon.tsx b/src/components/molecules/CartIcon/CartIcon.tsx index f18a7d6..48f2478 100644 --- a/src/components/molecules/CartIcon/CartIcon.tsx +++ b/src/components/molecules/CartIcon/CartIcon.tsx @@ -3,71 +3,127 @@ import styled from 'styled-components'; import { CartPlus } from '@styled-icons/fa-solid/CartPlus'; import Badge from '../../atoms/Badge/Badge'; import { COLOR_OPTIONS } from '../../../common/constants/consts'; +import './cartIcon.scss'; -interface ICartIcon { +interface ICartIconContainerStyle { + width?: number; + hideCartNumberLabel?: boolean; +} + +interface ICartIcon extends ICartIconContainerStyle { color?: string; isActive?: boolean; backgroundColor?: string; fontColor?: string; - width?: number; + scaleOnHover?: boolean; } -interface ICartIconContainer extends ICartIcon { +export interface ICartIconContainer extends ICartIcon { className?: string; - onCartIconClick?: () => void; - hideCartNumberLabel?: boolean; + onCartIconClick?: (product?: any) => void; numberOfItemsInCart?: number; } -export const CartIcon = styled(CartPlus)` - ${({ theme, color = COLOR_OPTIONS.primary, isActive, fontColor, backgroundColor, width = 40 }) => ({ +const defaultProps = { + color: COLOR_OPTIONS.primary, + width: 40, + hideCartNumberLabel: false, + numberOfItemsInCart: 0, + isActive: false, + scaleOnHover: false, +}; + +export const StyledCartIcon = styled(CartPlus)` + ${({ + theme, + color = defaultProps.color, + isActive, + fontColor, + backgroundColor, + width = defaultProps.width, + scaleOnHover = defaultProps.scaleOnHover, + }) => ({ color: fontColor || theme.colors[color].light, 'background-color': backgroundColor || theme.colors[color].base, width: `${width}px`, padding: '7px', 'border-radius': '5px', transition: 'transform .2s', - transform: isActive ? 'scale(1.2)' : 'scale(1)', + transform: isActive && !scaleOnHover ? 'scale(1.2)' : 'scale(1)', cursor: 'pointer', + '&:hover': { + transform: scaleOnHover ? 'scale(1.2)' : 'scale(1)', + }, })} `; -export default ({ +const CartIconWrapper = styled.div` + ${({ width = defaultProps.width, hideCartNumberLabel = defaultProps.hideCartNumberLabel }) => ({ + height: hideCartNumberLabel ? `${width}px` : `${width + 25}px`, + width: hideCartNumberLabel ? `${width}px` : `${width + 25}px`, + })} +`; + +const CartIcon = ({ color, - isActive = false, + isActive, onCartIconClick, hideCartNumberLabel, - numberOfItemsInCart = 0, + numberOfItemsInCart, className, fontColor, backgroundColor, - width + width, + scaleOnHover, }: ICartIconContainer) => { - const handleOnCartIconClick = (e: React.MouseEvent) => { + const handleOnCartIconClick = (e: React.MouseEvent, product: any) => { e.stopPropagation(); - onCartIconClick && onCartIconClick(); + onCartIconClick && onCartIconClick(product); }; if (hideCartNumberLabel || numberOfItemsInCart === 0) { return ( -
- + { + handleOnCartIconClick(e, product); + }} + className="cartIconContainer--nolabel" fontColor={fontColor} backgroundColor={backgroundColor} width={width} + scaleOnHover={scaleOnHover} /> -
+ ); } return ( -
- - - -
+ +
+ + { + handleOnCartIconClick(e, product); + }} + fontColor={fontColor} + backgroundColor={backgroundColor} + width={width} + scaleOnHover={scaleOnHover} + /> + +
+
); }; + +CartIcon.defaultProps = defaultProps; + +export default CartIcon; diff --git a/src/components/molecules/CartIcon/__snapshots__/CartIcon.spec.js.snap b/src/components/molecules/CartIcon/__snapshots__/CartIcon.spec.js.snap index 35ba987..2659ab1 100644 --- a/src/components/molecules/CartIcon/__snapshots__/CartIcon.spec.js.snap +++ b/src/components/molecules/CartIcon/__snapshots__/CartIcon.spec.js.snap @@ -1,13 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Cart Icon component should render correctly 1`] = ` -.c0 { +.c1 { display: inline-block; vertical-align: -.125em; overflow: hidden; } -.c1 { +.c2 { color: #ffffff; background-color: #000000; width: 40px; @@ -22,18 +22,37 @@ exports[`Cart Icon component should render correctly 1`] = ` cursor: pointer; } - + focusable="false" + onClick={[Function]} + viewBox="0 0 576 512" + width={40} + xmlns="http://www.w3.org/2000/svg" + > + + +
`; diff --git a/src/components/molecules/CartIcon/cartIcon.scss b/src/components/molecules/CartIcon/cartIcon.scss new file mode 100644 index 0000000..7a0f066 --- /dev/null +++ b/src/components/molecules/CartIcon/cartIcon.scss @@ -0,0 +1,14 @@ +.cartIconContainer--label { + position: absolute; + bottom: -12px; + left: -10px; + div { + margin: 0; + } +} + +.cartIconContainer--nolabel { + position: absolute; + bottom: 0; + left: 0; +} diff --git a/src/components/organisms/Wishlist/Wishlist.spec.js b/src/components/organisms/Wishlist/Wishlist.spec.js index 55ea9fe..aff6411 100644 --- a/src/components/organisms/Wishlist/Wishlist.spec.js +++ b/src/components/organisms/Wishlist/Wishlist.spec.js @@ -5,7 +5,7 @@ import 'jest-styled-components'; import { ThemeProvider } from 'styled-components'; import theme from '../../../common/theme'; import Wishlist from './Wishlist'; -import WishlistItem, {StyledWishlistDeleteIcon} from './WishlistItem'; +import WishlistItem, { StyledWishlistDeleteIcon } from './WishlistItem'; import wishlistProducts from '../../../common/mocks/wishlistProducts'; import setupIntersectionObserverMock from '../../../helpers/intersectionObserverMock'; import Text from '../../atoms/Text/Text'; @@ -15,6 +15,15 @@ describe('Wishlist component', () => { setupIntersectionObserverMock(); }); + it('should render correctly', () => { + const tree = create( + + + + ).toJSON(); + expect(tree).toMatchSnapshot() + }) + it('should render wishlist item with proper product data', () => { const item = create( @@ -85,20 +94,4 @@ describe('Wishlist component', () => { }) expect(mockFn.mock.calls.length).toEqual(1); }) - - it('should call onCartIconClick function', () => { - const mockFn = jest.fn(); - const e = { stopPropagation: jest.fn() }; - const wishlist = create( - - - - ) - const instance = wishlist.root; - const cartIcon = instance.findAllByType(CartIcon)[0]; - act(() => { - cartIcon.props.onClick(e); - }) - expect(mockFn.mock.calls.length).toEqual(1); - }) }); diff --git a/src/components/organisms/Wishlist/Wishlist.tsx b/src/components/organisms/Wishlist/Wishlist.tsx index 63ef986..e8ede7c 100644 --- a/src/components/organisms/Wishlist/Wishlist.tsx +++ b/src/components/organisms/Wishlist/Wishlist.tsx @@ -6,7 +6,7 @@ interface IWishlist extends IWishlistCommon { products: IWishlistProduct[]; } -const Wishlist = ({ className, products, color, nameTextProps, priceTextProps, itemClassName, onDeleteIconClick, onItemClick, deleteIconColor, hideDeleteIcon }: IWishlist) => ( +const Wishlist = ({ className, products, color, nameTextProps, priceTextProps, itemClassName, onDeleteIconClick, onItemClick, deleteIconColor, hideDeleteIcon, hideCartIcon, cartIconProps }: IWishlist) => (
{products.map((product, index) => ( ))}
diff --git a/src/components/organisms/Wishlist/WishlistItem.tsx b/src/components/organisms/Wishlist/WishlistItem.tsx index b1cc635..0804c49 100644 --- a/src/components/organisms/Wishlist/WishlistItem.tsx +++ b/src/components/organisms/Wishlist/WishlistItem.tsx @@ -5,25 +5,26 @@ import Image from '../../atoms/Image/Image'; import './wishlist.scss'; import { FONT_WEIGHT_OPTIONS } from '../../../common/constants/consts'; import { DeleteForever } from '@styled-icons/material-sharp/DeleteForever'; +import CartIcon, { ICartIconContainer } from '../../molecules/CartIcon/CartIcon'; export interface IWishlistProduct { name: string; price: string; image: string; + inCartCount?: number; } export interface IWishlistCommon { itemClassName?: string; - hideHeartDislike?: boolean; hideCartIcon?: boolean; color?: string; - onClick?: () => void; nameTextProps?: IText; priceTextProps?: IText; - onDeleteIconClick?: () => void; - onItemClick?: () => void; + onDeleteIconClick?: (product: IWishlistProduct) => void; + onItemClick?: (product: IWishlistProduct) => void; deleteIconColor?: string; hideDeleteIcon?: boolean; + cartIconProps?: ICartIconContainer; } interface IWishlistItem extends IWishlistCommon { @@ -36,12 +37,24 @@ export const StyledWishlistDeleteIcon = styled(DeleteForever)<{ color?: string, color: deleteIconColor || theme.colors[color].base, width: '30px', position: 'absolute', - bottom: '0', + top: '0', right: '0', cursor: 'pointer', })}` -const WishlistItem = ({ product, color, nameTextProps, priceTextProps, itemClassName, onDeleteIconClick, onItemClick, deleteIconColor, hideDeleteIcon }: IWishlistItem) => { +const WishlistItem = ({ + product, + color, + nameTextProps, + priceTextProps, + itemClassName, + onDeleteIconClick, + onItemClick, + deleteIconColor, + hideDeleteIcon, + cartIconProps, + hideCartIcon, +}: IWishlistItem) => { const nameProps = { color, fontWeight: FONT_WEIGHT_OPTIONS['600'] as FontWeights, @@ -51,18 +64,36 @@ const WishlistItem = ({ product, color, nameTextProps, priceTextProps, itemClass color, ...priceTextProps, }; + + const onAddToCart = () => { + cartIconProps?.onCartIconClick && cartIconProps.onCartIconClick(product) + } + + const cartProps = { + className: `wishlistItem__cartIcon ${cartIconProps && cartIconProps.className}`, + width: 32, + hideCartNumberLabel: true, + color, + numberOfItemsInCart: product.inCartCount || 0, + ...cartIconProps, + onCartIconClick: onAddToCart, + } + return ( -
+
{onItemClick && onItemClick(product)}}>
{product.name}
{product.name} {product.price} + { !hideCartIcon && ( + + )} { !hideDeleteIcon && ( { e.stopPropagation(); - onDeleteIconClick && onDeleteIconClick(); + onDeleteIconClick && onDeleteIconClick(product); }} /> )}
diff --git a/src/components/organisms/Wishlist/__snapshots__/Wishlist.spec.js.snap b/src/components/organisms/Wishlist/__snapshots__/Wishlist.spec.js.snap new file mode 100644 index 0000000..309dea2 --- /dev/null +++ b/src/components/organisms/Wishlist/__snapshots__/Wishlist.spec.js.snap @@ -0,0 +1,276 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Wishlist component should render correctly 1`] = ` +.c1 { + color: #000000; + font-size: 16px; + line-height: 1.5; + font-weight: 600; + text-align: justify; +} + +.c2 { + color: #000000; + font-size: 16px; + line-height: 1.5; + font-weight: normal; + text-align: justify; +} + +.c0 { + object-fit: cover; + width: 100%; + height: 100%; +} + +.c4 { + display: inline-block; + vertical-align: -.125em; + overflow: hidden; +} + +.c6 { + display: inline-block; + vertical-align: middle; + overflow: hidden; +} + +.c5 { + color: #ffffff; + background-color: #000000; + width: 32px; + padding: 7px; + border-radius: 5px; + -webkit-transition: -webkit-transform .2s; + -webkit-transition: transform .2s; + transition: transform .2s; + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + cursor: pointer; +} + +.c5:hover { + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); +} + +.c3 { + height: 32px; + width: 32px; +} + +.c7 { + color: #000000; + width: 30px; + position: absolute; + top: 0; + right: 0; + cursor: pointer; +} + +
+
+
+ Blue summer hat +
+
+

+ Blue summer hat +

+

+ 25.00 EUR +

+
+ +
+ +
+
+
+
+ Huge sombrero +
+
+

+ Huge sombrero +

+

+ 30.99 EUR +

+
+ +
+ +
+
+
+
+ Hunter hat +
+
+

+ Hunter hat +

+

+ 23.50 EUR +

+
+ +
+ +
+
+
+`; diff --git a/src/components/organisms/Wishlist/wishlist.scss b/src/components/organisms/Wishlist/wishlist.scss index 3fc193e..ef595cf 100644 --- a/src/components/organisms/Wishlist/wishlist.scss +++ b/src/components/organisms/Wishlist/wishlist.scss @@ -1,5 +1,5 @@ .wishlistItem { - height: 100px; + height: 120px; display: flex; padding: 10px; &:hover { @@ -9,7 +9,7 @@ .wishlistItem__imageWrapper { height: 100%; - width: 100px; + width: 120px; margin-right: 10px; } @@ -20,3 +20,9 @@ width: 100%; position: relative; } + +.wishlistItem__cartIcon { + position: absolute; + bottom: 0; + right: 0; +} diff --git a/src/stories/CartIcon.stories.jsx b/src/stories/CartIcon.stories.jsx index 6bd6323..b103a72 100644 --- a/src/stories/CartIcon.stories.jsx +++ b/src/stories/CartIcon.stories.jsx @@ -21,6 +21,7 @@ stories.add('common', () => { color={select('Color', COLOR_OPTIONS, COLOR_OPTIONS.primary)} width={number('Width', 40)} hideCartNumberLabel={boolean('Hide label', false)} + scaleOnHover={boolean('Scale on hover', false)} /> ); }); diff --git a/src/stories/Wishlist.stories.jsx b/src/stories/Wishlist.stories.jsx new file mode 100644 index 0000000..015cf81 --- /dev/null +++ b/src/stories/Wishlist.stories.jsx @@ -0,0 +1,77 @@ +import React, { useState } from 'react'; +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { withKnobs, select, boolean, number, color } from '@storybook/addon-knobs'; +import Wishlist from '../components/organisms/Wishlist/Wishlist'; +import wishlistProducts from '../common/mocks/wishlistProducts'; +import { COLOR_OPTIONS, FONT_WEIGHT_OPTIONS } from '../common/constants/consts'; +import './styles.scss'; + +const stories = storiesOf('Wishlist', module); +stories.addDecorator(withKnobs); + +const productName = 'Product name'; +const price = 'Price'; +const main = 'Main'; +const cartIcon = 'Cart icon'; + +stories.add('common', () => { + const [products, setProducts] = useState(wishlistProducts); + + const onProductDelete = (product) => { + const newProducts = products.filter((item) => item.id !== product.id); + setProducts(newProducts); + }; + + const onAddToCart = (product) => { + const newProducts = products.map((item) => { + if (item.id === product.id) { + return { + ...item, + inCartCount: item.inCartCount + 1, + }; + } + return item; + }); + setProducts(newProducts); + }; + + return ( + + ); +}); diff --git a/src/stories/styles.scss b/src/stories/styles.scss index 3b257ec..b381dbe 100644 --- a/src/stories/styles.scss +++ b/src/stories/styles.scss @@ -7,6 +7,9 @@ flex-direction: column; align-items: flex-start; font-family: 'Montserrat', sans-serif; + * { + box-sizing: border-box; + } } .active { @@ -46,3 +49,7 @@ top: 50px; left: 50px; } + +.wishlist { + width: 50%; +}