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/ProducComponents.tsx b/src/02-component-patterns/components/ProducComponents.tsx new file mode 100644 index 000000000..04ebd1ebb --- /dev/null +++ b/src/02-component-patterns/components/ProducComponents.tsx @@ -0,0 +1,36 @@ +import { useContext } from "react" +import ProductContext from "./productContext" +import styles from '../styles/styles.module.css' +import noImage from '../assets/no-image.jpg' +import { Props as ProductImageProps } from "./ProductImage" +import { Props as ProductTitleProps } from "./ProductTitle" +import { Props as ProdtuctButtonsProps } from "./ProductButtons" + + +export const ProductImage = ({img='', className = '', style }:ProductImageProps):JSX.Element => { + const {product} = useContext(ProductContext) + const { img:imageContext } = product + return Coffee Mug +} + +export const ProductTitle = ({title = '', className = '', style}: ProductTitleProps):JSX.Element => { + const {product} = useContext(ProductContext) + const { title: titleContext } = product + return { title ? title : titleContext } +} +export const ProductButtons = ({className = '', style}: ProdtuctButtonsProps):JSX.Element => { + + const {counter, increasedBy} = useContext(ProductContext) + + return ( +
+ +
{counter}
+ +
+ ) +} \ No newline at end of file diff --git a/src/02-component-patterns/components/ProductButtons.tsx b/src/02-component-patterns/components/ProductButtons.tsx new file mode 100644 index 000000000..edcb24b4d --- /dev/null +++ b/src/02-component-patterns/components/ProductButtons.tsx @@ -0,0 +1,29 @@ +import { CSSProperties, useContext } from "react"; +import ProductContext from "./productContext"; + +import styles from '../styles/styles.module.css' + + +export interface Props { + className?: string + style?: CSSProperties +} + +export const ProductButtons = ({className, style}:Props) => { + + const { increasedBy, 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..4bd195882 --- /dev/null +++ b/src/02-component-patterns/components/ProductCard.tsx @@ -0,0 +1,38 @@ +import styles from '../styles/styles.module.css' +import { useProduct } from '../hooks/useProduct'; +import {ProductProps } from '../interfaces'; +import ProductContext from './productContext'; + +import '../styles/custom-styles.css' +import { CSSProperties, useEffect } from 'react'; +import { Product } from '../interfaces/interfaces'; + +export interface Props extends ProductProps { + className?: string + style?: CSSProperties + onChange?: (count:number, product: Product ) => void + value?: number +} + +export const ProductCard = ({product, children, className, style, onChange, value }:Props):JSX.Element => { + const { Provider } = ProductContext + // console.log(styles) + // console.log(className) + + + const {counter, increasedBy} = useProduct({ initialState: 0 , 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..2cd5be8ad --- /dev/null +++ b/src/02-component-patterns/components/ProductImage.tsx @@ -0,0 +1,30 @@ +import { CSSProperties, useContext } from 'react'; +import ProductContext from './productContext'; + +import styles from '../styles/styles.module.css' +import noImage from '../assets/no-image.jpg'; + +export interface Props { + img?: string + className?: string + style?: 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..3c3a9d7b9 --- /dev/null +++ b/src/02-component-patterns/components/ProductTitle.tsx @@ -0,0 +1,22 @@ +import { CSSProperties, useContext } from 'react'; +import ProductContext from './productContext'; + +import styles from '../styles/styles.module.css' +import '../styles/custom-styles.css' + +export interface Props { + title?: string; + className?: string + style?: 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..3cad2f0cc --- /dev/null +++ b/src/02-component-patterns/components/index.ts @@ -0,0 +1,14 @@ +import { ProductCard as ProductCardHOC } from "./ProductCard"; +import { ProductImage, ProductTitle, ProductButtons } from "./ProducComponents"; +// import { IProductCard } from "../interfaces"; +import { ProductCardHOCProps, ProductCardProps } from "../interfaces/interfaces"; + +export const ProductCard:ProductCardHOCProps = Object.assign(ProductCardHOC, { + Image: ProductImage, + Title: ProductTitle, + Buttons: ProductButtons, +}); + +export default ProductCard; +export * from "./ProducComponents"; +export { default as ProductContext } from "./productContext"; diff --git a/src/02-component-patterns/components/productContext.ts b/src/02-component-patterns/components/productContext.ts new file mode 100644 index 000000000..ce4d4c9d2 --- /dev/null +++ b/src/02-component-patterns/components/productContext.ts @@ -0,0 +1,6 @@ +import { createContext } from "react"; +import { ProductContextProps } from "../interfaces"; + +const ProductContext = createContext({} as ProductContextProps) + +export default ProductContext \ No newline at end of file diff --git a/src/02-component-patterns/data/products.ts b/src/02-component-patterns/data/products.ts new file mode 100644 index 000000000..16c7ea323 --- /dev/null +++ b/src/02-component-patterns/data/products.ts @@ -0,0 +1,18 @@ +import { Product } from "../interfaces/interfaces"; + +export const products:Product[] = [ + { + id: '1', + title: 'Coffee Mug | Card 1', + img: './coffee-mug.png' + }, + { + id: '2', + title: 'Coffee Mug | Card 2', + img: './coffee-mug2.png' + }, + // { + // id: '3', + // title: 'Coffee Mug | Card 3', + // }, +] \ No newline at end of file diff --git a/src/02-component-patterns/hooks/useProduct.tsx b/src/02-component-patterns/hooks/useProduct.tsx new file mode 100644 index 000000000..a20f7f0a2 --- /dev/null +++ b/src/02-component-patterns/hooks/useProduct.tsx @@ -0,0 +1,45 @@ +import { useState, useEffect, useRef } from "react"; +import { Product } from "../interfaces/interfaces"; + +type UseProductType = { + counter: number; + increasedBy: (value: number) => void; +}; + +type OnChangeType = (count: number, product: Product) => void; + +type UseProductArgsType = { + initialState: number; + onChange?: OnChangeType; + product?: Product; + value?: number; +}; + +export const useProduct = ({ + initialState, + onChange, + product, + value, +}: UseProductArgsType): UseProductType => { + const [counter, setcounter] = useState(initialState); + + const isControlled = useRef(!!onChange); + // console.log("hook", value, counter, !!onChange, !!product); + const increasedBy = (value: number): void => { + if (isControlled.current) { + onChange!(value, product!); + return; + } + const newValue = Math.max(counter + value, 0); + setcounter(newValue); + }; + + useEffect(() => { + setcounter(value!); + }, [value, product]); + + return { + counter, + increasedBy, + }; +}; diff --git a/src/02-component-patterns/hooks/useShoppingCart.tsx b/src/02-component-patterns/hooks/useShoppingCart.tsx new file mode 100644 index 000000000..7c0331d09 --- /dev/null +++ b/src/02-component-patterns/hooks/useShoppingCart.tsx @@ -0,0 +1,43 @@ +import { useState } from 'react' +import { Product, ProductsCart } from '../interfaces/interfaces' + +type ProductInCart = {[key:string] : ProductsCart} + + +type UseShoppingCartResponse = { + onProductCountChange: (count: number, product: Product) => void; + shoppingCart: ProductInCart; +} + +export const useShoppingCart = (): UseShoppingCartResponse => { + + const [shoppingCart, setShoppingCart] = useState({ }) + + const onProductCountChange = (count:number, product:Product) => { + setShoppingCart((prev) => { + // console.log(count, product.id) + + const productInCart: ProductsCart = prev[product.id] || { ...product, count:0 } + + if( Math.max(productInCart.count + count, 0) > 0 ){ + productInCart.count += count + return { + ...prev, + [productInCart.id]: productInCart + } + } + /* Borra elemento del carrito */ + // const cartWithoutZeros = { ...shoppingCart } + // delete cartWithoutZeros[product.id] + const { [product.id] : toDelete, ...rest } = prev + return {...rest } + + + }) +} + + return { + onProductCountChange, + shoppingCart + } +} diff --git a/src/02-component-patterns/interfaces/index.ts b/src/02-component-patterns/interfaces/index.ts new file mode 100644 index 000000000..fc765a720 --- /dev/null +++ b/src/02-component-patterns/interfaces/index.ts @@ -0,0 +1 @@ +export * from './productsInterfaces' \ 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..8db04a5d7 --- /dev/null +++ b/src/02-component-patterns/interfaces/interfaces.ts @@ -0,0 +1,36 @@ +import { CSSProperties, ReactElement } from "react"; +import { Props as ProductCardType } from "../components/ProductCard"; +import { Props as PropsProductImage } from "../components/ProductImage"; +import { Props as PropsProductTitle } from "../components/ProductTitle"; + +export interface ProductCardProps { + product: Product; + children?: ReactElement | ReactElement[]; +} + +export interface Product { + id: string; + title: string; + img?: string; +} + +export interface ProductContextProps { + counter: number; + increaseBy: (value: number) => void; + product: Product; +} + +export interface ProductCardHOCProps { + ({ children, product, className }: ProductCardType): JSX.Element; + Title: (Props: PropsProductTitle) => JSX.Element; + Image: (Props: PropsProductImage) => JSX.Element; + Buttons: ({ + className, + }: { + className?: string; + style?: CSSProperties; + }) => JSX.Element; +} +export interface ProductsCart extends Product { + count: number; +} diff --git a/src/02-component-patterns/interfaces/productsInterfaces.ts b/src/02-component-patterns/interfaces/productsInterfaces.ts new file mode 100644 index 000000000..343809672 --- /dev/null +++ b/src/02-component-patterns/interfaces/productsInterfaces.ts @@ -0,0 +1,26 @@ +import { ReactNode } from "react"; + +export interface IProduct { + id: string; + title: string; + img?: string; + className?: string; +} + +export interface ProductProps { + product: IProduct; + children?: ReactNode; +} + +export interface ProductContextProps { + product: IProduct; + counter: number; + increasedBy: (value: number) => void; +} + +export interface IProductCard { + ({ product, children }: ProductProps): JSX.Element; + Image: ({ img }: { img?: string | undefined }) => JSX.Element; + Title: ({ title }: { title?: string | undefined }) => JSX.Element; + Buttons: () => JSX.Element; +} diff --git a/src/02-component-patterns/pages/ShoppingPage.tsx b/src/02-component-patterns/pages/ShoppingPage.tsx new file mode 100644 index 000000000..3483f4d72 --- /dev/null +++ b/src/02-component-patterns/pages/ShoppingPage.tsx @@ -0,0 +1,147 @@ +import{ FC } from 'react' +import { ProductButtons, ProductCard, ProductImage, ProductTitle } from '../components' +import '../styles/custom-styles.css' +import { products } from '../data/products'; +import { useShoppingCart } from '../hooks/useShoppingCart'; + + + +const ShoppingPage:FC = ():JSX.Element => { + // Record + const { shoppingCart, onProductCountChange } = useShoppingCart() + + return ( +
+

ShoppingPage

+
+
+ { products.map(product =>( + onProductCountChange(count, product)} + key={product.id} + > + + + + + ))} + + {/* + + + + + + + + + + + + + + */} +
+
+ { + ( + Object.entries(shoppingCart).map( ([key, product]) => ( + onProductCountChange(count, product)} + key={key} + > + + + + )) + ) + } + +
+ {/*
+ + {JSON.stringify(shoppingCart, null, 5)} + +
*/} +
+ ) +} + +export default ShoppingPage 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..11e2ad19a --- /dev/null +++ b/src/02-component-patterns/styles/custom-styles.css @@ -0,0 +1,28 @@ +.bg-dark { + background-color: rgb(56, 56, 56) !important; +} + +.text-white { + color: white +} + +.text-bold { + font-weight: bold; +} + +.custom-image { + padding: 10px; + border-radius: 20px 20px 20px 20px;; + width: calc(100% - 20px); +} + +.custom-buttons button, .custom-buttons div { + border-color: white; + color:white; +} + +.shopping-cart { + position: fixed; + top: 0; + right: 10px; +} \ 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..defa29458 --- /dev/null +++ b/src/02-component-patterns/styles/styles.module.css @@ -0,0 +1,63 @@ + + +.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); +} + diff --git a/src/routes/Navigation.tsx b/src/routes/Navigation.tsx index a9bfe83b0..24301ec7e 100644 --- a/src/routes/Navigation.tsx +++ b/src/routes/Navigation.tsx @@ -4,9 +4,12 @@ import { Route, NavLink } from 'react-router-dom'; +import ShoppingPage from '../02-component-patterns/pages/ShoppingPage'; + import logo from '../logo.svg'; + export const Navigation = () => { return ( @@ -15,7 +18,7 @@ export const Navigation = () => { React Logo
  • - Home + Shopping
  • About @@ -36,7 +39,7 @@ export const Navigation = () => {

    Users

    -

    Home

    +