diff --git a/package.json b/package.json index d9a0e2130..e60ee60ae 100644 --- a/package.json +++ b/package.json @@ -44,4 +44,4 @@ "devDependencies": { "@types/react-router-dom": "^5.1.9" } -} +} \ No newline at end of file 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..9710477a6 --- /dev/null +++ b/src/02-component-patterns/components/ProductButtons.tsx @@ -0,0 +1,37 @@ +import { useContext } from 'react'; +import { type CSSProperties } from 'react'; + +import styles from '../styles/styles.module.css'; +import { ProductContext } from './ProductCard'; + + +export interface Props { + className?: string; + style?: 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..90bd2be20 --- /dev/null +++ b/src/02-component-patterns/components/ProductCard.tsx @@ -0,0 +1,39 @@ +import { createContext } from 'react'; +import { type ReactElement, type CSSProperties } from 'react'; +import { useProduct } from '../hooks/useProduct'; + +import styles from '../styles/styles.module.css'; + +import { Product, ProductContextProps, onChangeArgs } from '../interfaces/interfaces'; + +export const ProductContext = createContext({} as ProductContextProps); + +export interface Props { + product: Product; + children?: ReactElement | ReactElement[]; + className?: string; + style?: 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..b43bca54e --- /dev/null +++ b/src/02-component-patterns/components/ProductImage.tsx @@ -0,0 +1,37 @@ +import { useContext } from 'react'; +import { type CSSProperties } from 'react'; + +import { ProductContext } from './ProductCard'; + +import noImage from '../assets/no-image.jpg'; + +import styles from '../styles/styles.module.css'; + +export interface Props { + className?: string; + img?: string; + style?: CSSProperties; +} + +export const ProductImage = ({ className, img, style }: Props) => { + + const { product } = useContext(ProductContext); + + let imgToShow: string; + + if (img) { + imgToShow = img + } else if (product.img) { + imgToShow = product.img + } else { + imgToShow = noImage; + } + + return ( + Coffe Mug + ) +} \ 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..400e546e7 --- /dev/null +++ b/src/02-component-patterns/components/ProductTitle.tsx @@ -0,0 +1,26 @@ +import {useContext } from 'react'; +import { type CSSProperties } from 'react'; + +import { ProductContext } from './ProductCard'; + +import styles from '../styles/styles.module.css'; + +export interface Props { + className?: string; + title?: string; + style?: CSSProperties; +} + +export const ProductTitle = ({ title, className, style }: Props) => { + + const { product } = useContext(ProductContext); + + return ( + + {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..6330ad851 --- /dev/null +++ b/src/02-component-patterns/components/index.ts @@ -0,0 +1,16 @@ +import { ProductCard as ProductCardComponent } from './ProductCard'; + +import { ProductImage } from './ProductImage'; +import { ProductTitle } from './ProductTitle' +import { ProductButtons } from './ProductButtons'; +import { ProductCardHOCProps } from '../interfaces/interfaces'; + +export { ProductImage } from './ProductImage'; +export { ProductTitle } from './ProductTitle' +export { ProductButtons } from './ProductButtons'; + +export const ProductCard: ProductCardHOCProps = Object.assign(ProductCardComponent, { + Title : ProductTitle, + Image : ProductImage, + Buttons : ProductButtons, +}) \ 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..2e20f7ee1 --- /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', + img: './coffee-mug.png' +} + +const product2 = { + id: '2', + title: 'Coffee - Meme', + img: './coffee-mug2.png' +} + +export const products: Array = [product1, product2]; diff --git a/src/02-component-patterns/hooks/useProduct.ts b/src/02-component-patterns/hooks/useProduct.ts new file mode 100644 index 000000000..c181cfcc3 --- /dev/null +++ b/src/02-component-patterns/hooks/useProduct.ts @@ -0,0 +1,37 @@ +import { useState, useEffect, useRef } from 'react'; +import { Product, onChangeArgs } from '../interfaces/interfaces'; + +interface useProductArgs { + product: Product; + onChange?: (args: onChangeArgs) => void; + value?: number; +} + +export const useProduct = ({ onChange, product, value = 0 }: useProductArgs) => { + + const [counter, setCouter] = 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) + + setCouter(newValue); + + onChange && onChange({ count: newValue, product }); + } + + useEffect(() => { + setCouter(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..77d4c9078 --- /dev/null +++ b/src/02-component-patterns/hooks/useShoppingCart.ts @@ -0,0 +1,32 @@ +import { useState } from 'react'; + +import { Product, ProductInCart } from '../interfaces/interfaces'; + +export const useShoppingCart = () => { + + const [shoppingCart, setShoppingCart] = useState<{ [key: string]: ProductInCart }>({}); + + const onChangeProduct = ({ count, product }: { count: number, product: 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, + } + } + + const { [product.id]: toDelete, ...rest } = oldShoppingCart; + return rest; + + }); + } + + return { + shoppingCart, + onChangeProduct + } +} \ 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..f5ddba9a9 --- /dev/null +++ b/src/02-component-patterns/interfaces/interfaces.ts @@ -0,0 +1,32 @@ +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 { + (Props: ProductCardProps): JSX.Element; + Title: (Props: ProductTitleProps) => JSX.Element; + Image: (Props: ProductImageProps) => JSX.Element; + Buttons: (Props: ProductButtonsProps) => 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..814ca59b3 --- /dev/null +++ b/src/02-component-patterns/pages/ShoppingPage.tsx @@ -0,0 +1,60 @@ +import { ProductCard } from '../components/'; + +import { useShoppingCart } from '../hooks/useShoppingCart'; + +import { products } from '../data/products'; + +import '../styles/custom-styles.css'; + +const ShoppingPage = () => { + + const { onChangeProduct, shoppingCart } = useShoppingCart(); + + return ( +
+

Shopping Store

+
+ +
+ + { + products.map(product => ( + + + + + + )) + } + +
+ +
+ { + Object.entries(shoppingCart).map(([key, product]) => ( + + + + + + )) + } +
+
+ ) +} + +export default ShoppingPage \ No newline at end of file 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..0584c8ec1 --- /dev/null +++ b/src/02-component-patterns/styles/custom-styles.css @@ -0,0 +1,23 @@ +.bg-dark { + background-color: rgb(56, 56, 56); +} +.text-white { + color: white; +} + +.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; + top: 0px; + 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..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..8f0da96ba 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