diff --git a/public/coffee-mug.png b/public/coffee-mug.png new file mode 100644 index 000000000..c19c2088d Binary files /dev/null and b/public/coffee-mug.png differ diff --git a/public/coffee-mug2.png b/public/coffee-mug2.png new file mode 100644 index 000000000..531608948 Binary files /dev/null and b/public/coffee-mug2.png differ diff --git a/src/02-component-patterns/assets/no-image.jpg b/src/02-component-patterns/assets/no-image.jpg new file mode 100644 index 000000000..ae122b097 Binary files /dev/null and b/src/02-component-patterns/assets/no-image.jpg differ diff --git a/src/02-component-patterns/components/ProductButtons.tsx b/src/02-component-patterns/components/ProductButtons.tsx new file mode 100644 index 000000000..ae7bf94e6 --- /dev/null +++ b/src/02-component-patterns/components/ProductButtons.tsx @@ -0,0 +1,31 @@ +import { useContext } from "react"; +import { ProductContext } from "./ProductCard"; + +import styles from '../styles/styles.module.css' + +export interface Props { + className?: string; + style?: React.CSSProperties +} + +export const ProductButtons = ({ className, style }: Props) => { + + const { increaseBy, counter } = useContext( ProductContext ); + + return ( +
+ + +
{ counter }
+ + +
+ ); +} \ No newline at end of file diff --git a/src/02-component-patterns/components/ProductCard.tsx b/src/02-component-patterns/components/ProductCard.tsx new file mode 100644 index 000000000..74187d1c4 --- /dev/null +++ b/src/02-component-patterns/components/ProductCard.tsx @@ -0,0 +1,41 @@ +import { createContext } from 'react'; + +import { useProduct } from '../hooks/useProduct'; +import { ProductContextProps, Product, onChangeArgs } from '../interfaces/interfaces'; + +import styles from '../styles/styles.module.css' + +export const ProductContext = createContext({} as ProductContextProps); +const { Provider } = ProductContext; + + + +export interface Props { + product: Product; + children?: React.ReactElement | React.ReactElement[]; + className?: string; + style?: React.CSSProperties; + onChange?: ( args: onChangeArgs ) => void; + value?: number; +} + + +export const ProductCard = ({ children, product, className, style, onChange, value }: Props ) => { + + const { counter, increaseBy } = useProduct({ onChange, product, value }); + + return ( + +
+ { children } +
+
+ ) +} diff --git a/src/02-component-patterns/components/ProductImage.tsx b/src/02-component-patterns/components/ProductImage.tsx new file mode 100644 index 000000000..cc777c41e --- /dev/null +++ b/src/02-component-patterns/components/ProductImage.tsx @@ -0,0 +1,36 @@ +import { useContext } from 'react'; +import { ProductContext } from './ProductCard'; + +import styles from '../styles/styles.module.css' +import noImage from '../assets/no-image.jpg'; + +export interface Props { + img?: string; + className?: string; + style?: React.CSSProperties +} + + +export const ProductImage = ({ img, className, style }: Props ) => { + + const { product } = useContext( ProductContext ); + let imgToShow: string; + + if ( img ) { + imgToShow = img; + } else if ( product.img ) { + imgToShow = product.img + } else { + imgToShow = noImage; + } + + + return ( + Product + ); +} \ No newline at end of file diff --git a/src/02-component-patterns/components/ProductTitle.tsx b/src/02-component-patterns/components/ProductTitle.tsx new file mode 100644 index 000000000..85454fd4d --- /dev/null +++ b/src/02-component-patterns/components/ProductTitle.tsx @@ -0,0 +1,26 @@ +import { useContext } from 'react'; +import { ProductContext } from "./ProductCard"; + +import styles from '../styles/styles.module.css' + + +export interface Props { + className?: string + title?: string, + activeClass?: string; + style?: React.CSSProperties +} + +export const ProductTitle = ({ title, className, style }: Props) => { + + const { product } = useContext( ProductContext ) + + return ( + + { title ? title : product.title } + + ); +} \ No newline at end of file diff --git a/src/02-component-patterns/components/index.ts b/src/02-component-patterns/components/index.ts new file mode 100644 index 000000000..579387b8b --- /dev/null +++ b/src/02-component-patterns/components/index.ts @@ -0,0 +1,21 @@ +import { ProductCard as ProductCardHOC } from './ProductCard'; +import { ProductCardHOCProps } from '../interfaces/interfaces'; + +import { ProductButtons } from './ProductButtons'; +import { ProductImage } from './ProductImage'; +import { ProductTitle } from './ProductTitle'; + +export { ProductButtons } from './ProductButtons'; +export { ProductImage } from './ProductImage'; +export { ProductTitle } from './ProductTitle'; + + +export const ProductCard: ProductCardHOCProps = Object.assign( ProductCardHOC, { + Title: ProductTitle, + Image: ProductImage, + Buttons: ProductButtons +}) + + +export default ProductCard; + diff --git a/src/02-component-patterns/data/products.ts b/src/02-component-patterns/data/products.ts new file mode 100644 index 000000000..c7f21c651 --- /dev/null +++ b/src/02-component-patterns/data/products.ts @@ -0,0 +1,15 @@ +import { Product } from '../interfaces/interfaces'; + +const product1 = { + id: '1', + title: 'Coffee Mug - Card', + img: './coffee-mug.png' +} + +const product2 = { + id: '2', + title: 'Coffee Mug - Meme', + img: './coffee-mug2.png' +} + +export const products: Product[] = [ product1, product2 ]; \ No newline at end of file diff --git a/src/02-component-patterns/hooks/useProduct.ts b/src/02-component-patterns/hooks/useProduct.ts new file mode 100644 index 000000000..a0bda3bfc --- /dev/null +++ b/src/02-component-patterns/hooks/useProduct.ts @@ -0,0 +1,39 @@ +import { useEffect, useRef, useState } from 'react' +import { onChangeArgs, Product } from '../interfaces/interfaces'; + + +interface useProductArgs { + product: Product; + onChange?: ( args: onChangeArgs ) => void; + value?: number; +} + + +export const useProduct = ({ onChange, product, value = 0 }: useProductArgs) => { + + const [ counter, setCounter ] = useState( value ); + + const isControlled = useRef( !!onChange ) + + const increaseBy = ( value: number ) => { + + if( isControlled.current ) { + return onChange!({ count: value, product }); + } + + const newValue = Math.max( counter + value, 0 ) + setCounter( newValue ); + + onChange && onChange({ count: newValue, product }); + } + + useEffect(() => { + setCounter( value ); + }, [ value ]) + + return { + counter, + increaseBy + } + +} \ No newline at end of file diff --git a/src/02-component-patterns/hooks/useShoppingCart.ts b/src/02-component-patterns/hooks/useShoppingCart.ts new file mode 100644 index 000000000..0c922bee0 --- /dev/null +++ b/src/02-component-patterns/hooks/useShoppingCart.ts @@ -0,0 +1,48 @@ +import { useState } from 'react'; +import { Product, ProductInCart } from '../interfaces/interfaces'; + + + +export const useShoppingCart = () => { + + const [ shoppingCart, setShoppingCart ] = useState<{ [key:string]: ProductInCart }>({}); + + const onProductCountChange = ({ count, product }: { count:number, product: Product }) => { + // console.log( count, product); + + setShoppingCart( oldShoppingCart => { + + const productInCart: ProductInCart = oldShoppingCart[product.id] || { ...product, count: 0 }; + + if( Math.max( productInCart.count + count, 0 ) > 0 ) { + productInCart.count += count; + return { + ...oldShoppingCart, + [product.id]: productInCart + } + } + + // Borrar el producto + const { [product.id]: toDelete, ...rest } = oldShoppingCart; + return rest; + + + // if( count === 0 ) { + // const { [product.id]: toDelete, ...rest } = oldShoppingCart; + // return rest; + // } + + // return { + // ...oldShoppingCart, + // [ product.id ]: { ...product, count } + // } + }) + + } + + return { + shoppingCart, + onProductCountChange, + } + +} \ No newline at end of file diff --git a/src/02-component-patterns/interfaces/interfaces.ts b/src/02-component-patterns/interfaces/interfaces.ts new file mode 100644 index 000000000..89f5a80be --- /dev/null +++ b/src/02-component-patterns/interfaces/interfaces.ts @@ -0,0 +1,36 @@ +import { Props as ProductButtonsProps } from '../components/ProductButtons'; +import { Props as ProductCardProps } from '../components/ProductCard'; +import { Props as ProductImageProps } from '../components/ProductImage'; +import { Props as ProductTitleProps } from '../components/ProductTitle'; + + +export interface Product { + id: string; + img?: string; + title: string; +} + +export interface ProductContextProps { + counter: number; + product: Product; + increaseBy: ( value: number ) => void; +} + + +export interface ProductCardHOCProps { + ({ children, product }: ProductCardProps ):JSX.Element, + Buttons: ( Props: ProductButtonsProps ) => JSX.Element, + Image: ( Props: ProductImageProps ) => JSX.Element, + Title: ( Props: ProductTitleProps ) => JSX.Element, +} + + +export interface onChangeArgs { + product: Product; + count: number; +} + + +export interface ProductInCart extends Product { + count: number +} \ No newline at end of file diff --git a/src/02-component-patterns/pages/ShoppingPage.tsx b/src/02-component-patterns/pages/ShoppingPage.tsx new file mode 100644 index 000000000..2540e07d8 --- /dev/null +++ b/src/02-component-patterns/pages/ShoppingPage.tsx @@ -0,0 +1,74 @@ +import { ProductCard, ProductImage, ProductTitle, ProductButtons } from '../components'; +import { useShoppingCart } from '../hooks/useShoppingCart'; + +import { products } from '../data/products'; +import '../styles/custom-styles.css'; + + + + + +export const ShoppingPage = () => { + + const { shoppingCart, onProductCountChange } = useShoppingCart(); + + + return ( +
+

Shopping Store

+
+ +
+ + + { + products.map( product => ( + + + + + + )) + } +
+ +
+ + { + Object.entries( shoppingCart ).map( ([ key, product ]) => ( + + + + + )) + } + + +
+ +
+ ) +} diff --git a/src/02-component-patterns/styles/custom-styles.css b/src/02-component-patterns/styles/custom-styles.css new file mode 100644 index 000000000..f5692c82a --- /dev/null +++ b/src/02-component-patterns/styles/custom-styles.css @@ -0,0 +1,31 @@ + +.bg-dark { + background-color: rgb(56,56,56); +} + +.text-white { + color: white; +} + +.text-bold { + font-weight: bold; +} + +.custom-image { + border-radius: 20px; + padding: 10px; + width: calc( 100% - 20px ); +} + + +.custom-buttons button, .custom-buttons div { + color: white; + border-color: white; +} + + +.shopping-cart { + position: fixed; + right: 10px; + top: 0px; +} \ No newline at end of file diff --git a/src/02-component-patterns/styles/styles.module.css b/src/02-component-patterns/styles/styles.module.css new file mode 100644 index 000000000..48ab3f819 --- /dev/null +++ b/src/02-component-patterns/styles/styles.module.css @@ -0,0 +1,62 @@ + + +.productCard { + background-color: white; + border-radius: 15px; + color: black; + padding-bottom: 5px; + width: 250px; + margin-right: 5px; + margin-top: 5px; +} + +.productImg { + border-radius: 15px 15px 0px 0px; + width: 100%; +} + +.productDescription { + margin: 10px; +} + +.buttonsContainer { + margin: 10px; + display: flex; + flex-direction: row; +} + +.buttonMinus { + cursor: pointer; + background-color: transparent; + border: 1px solid black; + border-radius: 5px 0px 0px 5px; + font-size: 20px; + width: 30px; +} + +.buttonMinus:hover { + background-color: rgba(0, 0, 0, 0.1); +} + +.countLabel { + border-bottom: 1px solid black; + border-top: 1px solid black; + font-size: 16px; + height: 25px; + padding-top: 5px; + text-align: center; + width: 30px; +} + +.buttonAdd { + cursor: pointer; + background-color: transparent; + border: 1px solid black; + border-radius: 0px 5px 5px 0px; + font-size: 20px; + width: 30px; +} + +.buttonAdd:hover { + background-color: rgba(0, 0, 0, 0.1); +} \ No newline at end of file diff --git a/src/routes/Navigation.tsx b/src/routes/Navigation.tsx index a9bfe83b0..4d59ed414 100644 --- a/src/routes/Navigation.tsx +++ b/src/routes/Navigation.tsx @@ -6,6 +6,7 @@ import { } from 'react-router-dom'; import logo from '../logo.svg'; +import { ShoppingPage } from '../02-component-patterns/pages/ShoppingPage'; export const Navigation = () => { return ( @@ -15,7 +16,7 @@ export const Navigation = () => { React Logo