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 (
+
+ );
+}
\ 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 = () => {
-
- Home
+ Shopping
-
About
@@ -36,7 +37,7 @@ export const Navigation = () => {
Users
- Home
+