diff --git a/code/01-starting-project/package.json b/code/01-starting-project/package.json new file mode 100644 index 0000000000..b16a8faa39 --- /dev/null +++ b/code/01-starting-project/package.json @@ -0,0 +1,38 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-scripts": "4.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/01-starting-project/public/favicon.ico b/code/01-starting-project/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/01-starting-project/public/favicon.ico differ diff --git a/code/01-starting-project/public/index.html b/code/01-starting-project/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/01-starting-project/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/code/01-starting-project/public/logo192.png b/code/01-starting-project/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/01-starting-project/public/logo192.png differ diff --git a/code/01-starting-project/public/logo512.png b/code/01-starting-project/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/01-starting-project/public/logo512.png differ diff --git a/code/01-starting-project/public/manifest.json b/code/01-starting-project/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/01-starting-project/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/01-starting-project/public/robots.txt b/code/01-starting-project/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/01-starting-project/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/01-starting-project/src/App.js b/code/01-starting-project/src/App.js new file mode 100644 index 0000000000..425f59acd7 --- /dev/null +++ b/code/01-starting-project/src/App.js @@ -0,0 +1,14 @@ +import Cart from './components/Cart/Cart'; +import Layout from './components/Layout/Layout'; +import Products from './components/Shop/Products'; + +function App() { + return ( + + + + + ); +} + +export default App; diff --git a/code/01-starting-project/src/components/Cart/Cart.js b/code/01-starting-project/src/components/Cart/Cart.js new file mode 100644 index 0000000000..ff9572d1f5 --- /dev/null +++ b/code/01-starting-project/src/components/Cart/Cart.js @@ -0,0 +1,18 @@ +import Card from '../UI/Card'; +import classes from './Cart.module.css'; +import CartItem from './CartItem'; + +const Cart = (props) => { + return ( + +

Your Shopping Cart

+ +
+ ); +}; + +export default Cart; diff --git a/code/01-starting-project/src/components/Cart/Cart.module.css b/code/01-starting-project/src/components/Cart/Cart.module.css new file mode 100644 index 0000000000..95670ab70b --- /dev/null +++ b/code/01-starting-project/src/components/Cart/Cart.module.css @@ -0,0 +1,16 @@ +.cart { + max-width: 30rem; + background-color: #313131; + color: white; +} + +.cart h2 { + font-size: 1.25rem; + margin: 0.5rem 0; +} + +.cart ul { + list-style: none; + padding: 0; + margin: 0; +} \ No newline at end of file diff --git a/code/01-starting-project/src/components/Cart/CartButton.js b/code/01-starting-project/src/components/Cart/CartButton.js new file mode 100644 index 0000000000..4af1e8c3a6 --- /dev/null +++ b/code/01-starting-project/src/components/Cart/CartButton.js @@ -0,0 +1,12 @@ +import classes from './CartButton.module.css'; + +const CartButton = (props) => { + return ( + + ); +}; + +export default CartButton; diff --git a/code/01-starting-project/src/components/Cart/CartButton.module.css b/code/01-starting-project/src/components/Cart/CartButton.module.css new file mode 100644 index 0000000000..93445f4596 --- /dev/null +++ b/code/01-starting-project/src/components/Cart/CartButton.module.css @@ -0,0 +1,21 @@ +.button { + background-color: transparent; + border-color: #1ad1b9; + color: #1ad1b9; +} + +.button:hover, +.button:active { + color: white; +} + +.button span { + margin: 0 0.5rem; +} + +.badge { + background-color: #1ad1b9; + border-radius: 30px; + padding: 0.15rem 1.25rem; + color: #1d1d1d; +} \ No newline at end of file diff --git a/code/01-starting-project/src/components/Cart/CartItem.js b/code/01-starting-project/src/components/Cart/CartItem.js new file mode 100644 index 0000000000..22d34df192 --- /dev/null +++ b/code/01-starting-project/src/components/Cart/CartItem.js @@ -0,0 +1,28 @@ +import classes from './CartItem.module.css'; + +const CartItem = (props) => { + const { title, quantity, total, price } = props.item; + + return ( +
  • +
    +

    {title}

    +
    + ${total.toFixed(2)}{' '} + (${price.toFixed(2)}/item) +
    +
    +
    +
    + x {quantity} +
    +
    + + +
    +
    +
  • + ); +}; + +export default CartItem; diff --git a/code/01-starting-project/src/components/Cart/CartItem.module.css b/code/01-starting-project/src/components/Cart/CartItem.module.css new file mode 100644 index 0000000000..e34100d841 --- /dev/null +++ b/code/01-starting-project/src/components/Cart/CartItem.module.css @@ -0,0 +1,58 @@ +.item { + margin: 1rem 0; + background-color: #575757; + padding: 1rem; +} + +.item h3 { + margin: 0 0 0.5rem 0; + font-size: 1.75rem; +} + +.item header { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.details { + display: flex; + justify-content: space-between; + align-items: center; +} + +.quantity span { + font-size: 1.5rem; + font-weight: bold; +} + +.price { + font-size: 1.5rem; + font-weight: bold; +} + +.itemprice { + font-weight: normal; + font-size: 1rem; + font-style: italic; +} + +.actions { + display: flex; + justify-content: flex-end; + margin: 0.5rem 0; +} + +.actions button { + background-color: transparent; + border: 1px solid white; + margin-left: 0.5rem; + padding: 0.15rem 1rem; + color: white; +} + +.actions button:hover, +.actions button:active { + background-color: #4b4b4b; + color: white; +} \ No newline at end of file diff --git a/code/01-starting-project/src/components/Layout/Layout.js b/code/01-starting-project/src/components/Layout/Layout.js new file mode 100644 index 0000000000..0ce12a011f --- /dev/null +++ b/code/01-starting-project/src/components/Layout/Layout.js @@ -0,0 +1,13 @@ +import { Fragment } from 'react'; +import MainHeader from './MainHeader'; + +const Layout = (props) => { + return ( + + +
    {props.children}
    +
    + ); +}; + +export default Layout; diff --git a/code/01-starting-project/src/components/Layout/MainHeader.js b/code/01-starting-project/src/components/Layout/MainHeader.js new file mode 100644 index 0000000000..38ea37a29b --- /dev/null +++ b/code/01-starting-project/src/components/Layout/MainHeader.js @@ -0,0 +1,19 @@ +import CartButton from '../Cart/CartButton'; +import classes from './MainHeader.module.css'; + +const MainHeader = (props) => { + return ( +
    +

    ReduxCart

    + +
    + ); +}; + +export default MainHeader; diff --git a/code/01-starting-project/src/components/Layout/MainHeader.module.css b/code/01-starting-project/src/components/Layout/MainHeader.module.css new file mode 100644 index 0000000000..e41c98d247 --- /dev/null +++ b/code/01-starting-project/src/components/Layout/MainHeader.module.css @@ -0,0 +1,19 @@ +.header { + width: 100%; + height: 5rem; + padding: 0 10%; + display: flex; + align-items: center; + justify-content: space-between; + background-color: #252424; +} + +.header h1 { + color: white; +} + +.header ul { + list-style: none; + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/code/01-starting-project/src/components/Shop/ProductItem.js b/code/01-starting-project/src/components/Shop/ProductItem.js new file mode 100644 index 0000000000..52fff3065a --- /dev/null +++ b/code/01-starting-project/src/components/Shop/ProductItem.js @@ -0,0 +1,23 @@ +import Card from '../UI/Card'; +import classes from './ProductItem.module.css'; + +const ProductItem = (props) => { + const { title, price, description } = props; + + return ( +
  • + +
    +

    {title}

    +
    ${price.toFixed(2)}
    +
    +

    {description}

    +
    + +
    +
    +
  • + ); +}; + +export default ProductItem; diff --git a/code/01-starting-project/src/components/Shop/ProductItem.module.css b/code/01-starting-project/src/components/Shop/ProductItem.module.css new file mode 100644 index 0000000000..2fcdc5d8a6 --- /dev/null +++ b/code/01-starting-project/src/components/Shop/ProductItem.module.css @@ -0,0 +1,27 @@ +.item h3 { + margin: 0.5rem 0; + font-size: 1.25rem; +} + +.item header { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.price { + border-radius: 30px; + padding: 0.15rem 1.5rem; + background-color: #3a3a3a; + color: white; + font-size: 1.5rem; +} + +.item p { + color: #3a3a3a; +} + +.actions { + display: flex; + justify-content: flex-end; +} \ No newline at end of file diff --git a/code/01-starting-project/src/components/Shop/Products.js b/code/01-starting-project/src/components/Shop/Products.js new file mode 100644 index 0000000000..1f6869b296 --- /dev/null +++ b/code/01-starting-project/src/components/Shop/Products.js @@ -0,0 +1,19 @@ +import ProductItem from './ProductItem'; +import classes from './Products.module.css'; + +const Products = (props) => { + return ( +
    +

    Buy your favorite products

    + +
    + ); +}; + +export default Products; diff --git a/code/01-starting-project/src/components/Shop/Products.module.css b/code/01-starting-project/src/components/Shop/Products.module.css new file mode 100644 index 0000000000..d81c97330f --- /dev/null +++ b/code/01-starting-project/src/components/Shop/Products.module.css @@ -0,0 +1,12 @@ +.products h2 { + color: white; + margin: 2rem auto; + text-align: center; + text-transform: uppercase; +} + +.products ul { + list-style: none; + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/code/01-starting-project/src/components/UI/Card.js b/code/01-starting-project/src/components/UI/Card.js new file mode 100644 index 0000000000..849202f21c --- /dev/null +++ b/code/01-starting-project/src/components/UI/Card.js @@ -0,0 +1,13 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return ( +
    + {props.children} +
    + ); +}; + +export default Card; diff --git a/code/01-starting-project/src/components/UI/Card.module.css b/code/01-starting-project/src/components/UI/Card.module.css new file mode 100644 index 0000000000..ac9c6709f4 --- /dev/null +++ b/code/01-starting-project/src/components/UI/Card.module.css @@ -0,0 +1,8 @@ +.card { + margin: 1rem auto; + border-radius: 6px; + background-color: white; + padding: 1rem; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/01-starting-project/src/index.css b/code/01-starting-project/src/index.css new file mode 100644 index 0000000000..3431f5f884 --- /dev/null +++ b/code/01-starting-project/src/index.css @@ -0,0 +1,31 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'); + +* { + box-sizing: border-box; +} + +html { + font-family: 'Noto Sans JP', sans-serif; +} + +body { + margin: 0; + background-color: #3f3f3f; +} + +button { + font: inherit; + cursor: pointer; + padding: 0.5rem 1.5rem; + border-radius: 6px; + background-color: transparent; + color: #1a8ed1; + border: 1px solid #1a8ed1; +} + +button:hover, +button:active { + background-color: #1ac5d1; + border-color: #1ac5d1; + color: white; +} \ No newline at end of file diff --git a/code/01-starting-project/src/index.js b/code/01-starting-project/src/index.js new file mode 100644 index 0000000000..778ec1ba20 --- /dev/null +++ b/code/01-starting-project/src/index.js @@ -0,0 +1,7 @@ +import ReactDOM from 'react-dom/client'; + +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/code/02-refresher-practice-finished/package.json b/code/02-refresher-practice-finished/package.json new file mode 100644 index 0000000000..99c15d93ce --- /dev/null +++ b/code/02-refresher-practice-finished/package.json @@ -0,0 +1,40 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@reduxjs/toolkit": "^1.5.0", + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-redux": "^7.2.2", + "react-scripts": "4.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/02-refresher-practice-finished/public/favicon.ico b/code/02-refresher-practice-finished/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/02-refresher-practice-finished/public/favicon.ico differ diff --git a/code/02-refresher-practice-finished/public/index.html b/code/02-refresher-practice-finished/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/02-refresher-practice-finished/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/code/02-refresher-practice-finished/public/logo192.png b/code/02-refresher-practice-finished/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/02-refresher-practice-finished/public/logo192.png differ diff --git a/code/02-refresher-practice-finished/public/logo512.png b/code/02-refresher-practice-finished/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/02-refresher-practice-finished/public/logo512.png differ diff --git a/code/02-refresher-practice-finished/public/manifest.json b/code/02-refresher-practice-finished/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/02-refresher-practice-finished/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/02-refresher-practice-finished/public/robots.txt b/code/02-refresher-practice-finished/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/02-refresher-practice-finished/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/02-refresher-practice-finished/src/App.js b/code/02-refresher-practice-finished/src/App.js new file mode 100644 index 0000000000..8531b2d78c --- /dev/null +++ b/code/02-refresher-practice-finished/src/App.js @@ -0,0 +1,18 @@ +import { useSelector } from 'react-redux'; + +import Cart from './components/Cart/Cart'; +import Layout from './components/Layout/Layout'; +import Products from './components/Shop/Products'; + +function App() { + const showCart = useSelector((state) => state.ui.cartIsVisible); + + return ( + + {showCart && } + + + ); +} + +export default App; diff --git a/code/02-refresher-practice-finished/src/components/Cart/Cart.js b/code/02-refresher-practice-finished/src/components/Cart/Cart.js new file mode 100644 index 0000000000..33030d2089 --- /dev/null +++ b/code/02-refresher-practice-finished/src/components/Cart/Cart.js @@ -0,0 +1,31 @@ +import { useSelector } from 'react-redux'; + +import Card from '../UI/Card'; +import classes from './Cart.module.css'; +import CartItem from './CartItem'; + +const Cart = (props) => { + const cartItems = useSelector((state) => state.cart.items); + + return ( + +

    Your Shopping Cart

    + +
    + ); +}; + +export default Cart; diff --git a/code/02-refresher-practice-finished/src/components/Cart/Cart.module.css b/code/02-refresher-practice-finished/src/components/Cart/Cart.module.css new file mode 100644 index 0000000000..95670ab70b --- /dev/null +++ b/code/02-refresher-practice-finished/src/components/Cart/Cart.module.css @@ -0,0 +1,16 @@ +.cart { + max-width: 30rem; + background-color: #313131; + color: white; +} + +.cart h2 { + font-size: 1.25rem; + margin: 0.5rem 0; +} + +.cart ul { + list-style: none; + padding: 0; + margin: 0; +} \ No newline at end of file diff --git a/code/02-refresher-practice-finished/src/components/Cart/CartButton.js b/code/02-refresher-practice-finished/src/components/Cart/CartButton.js new file mode 100644 index 0000000000..4e59baac3c --- /dev/null +++ b/code/02-refresher-practice-finished/src/components/Cart/CartButton.js @@ -0,0 +1,22 @@ +import { useDispatch, useSelector } from 'react-redux'; + +import { uiActions } from '../../store/ui-slice'; +import classes from './CartButton.module.css'; + +const CartButton = (props) => { + const dispatch = useDispatch(); + const cartQuantity = useSelector((state) => state.cart.totalQuantity); + + const toggleCartHandler = () => { + dispatch(uiActions.toggle()); + }; + + return ( + + ); +}; + +export default CartButton; diff --git a/code/02-refresher-practice-finished/src/components/Cart/CartButton.module.css b/code/02-refresher-practice-finished/src/components/Cart/CartButton.module.css new file mode 100644 index 0000000000..93445f4596 --- /dev/null +++ b/code/02-refresher-practice-finished/src/components/Cart/CartButton.module.css @@ -0,0 +1,21 @@ +.button { + background-color: transparent; + border-color: #1ad1b9; + color: #1ad1b9; +} + +.button:hover, +.button:active { + color: white; +} + +.button span { + margin: 0 0.5rem; +} + +.badge { + background-color: #1ad1b9; + border-radius: 30px; + padding: 0.15rem 1.25rem; + color: #1d1d1d; +} \ No newline at end of file diff --git a/code/02-refresher-practice-finished/src/components/Cart/CartItem.js b/code/02-refresher-practice-finished/src/components/Cart/CartItem.js new file mode 100644 index 0000000000..e1c6a8f012 --- /dev/null +++ b/code/02-refresher-practice-finished/src/components/Cart/CartItem.js @@ -0,0 +1,47 @@ +import { useDispatch } from 'react-redux'; + +import classes from './CartItem.module.css'; +import { cartActions } from '../../store/cart-slice'; + +const CartItem = (props) => { + const dispatch = useDispatch(); + + const { title, quantity, total, price, id } = props.item; + + const removeItemHandler = () => { + dispatch(cartActions.removeItemFromCart(id)); + }; + + const addItemHandler = () => { + dispatch( + cartActions.addItemToCart({ + id, + title, + price, + }) + ); + }; + + return ( +
  • +
    +

    {title}

    +
    + ${total.toFixed(2)}{' '} + (${price.toFixed(2)}/item) +
    +
    +
    +
    + x {quantity} +
    +
    + + +
    +
    +
  • + ); +}; + +export default CartItem; diff --git a/code/02-refresher-practice-finished/src/components/Cart/CartItem.module.css b/code/02-refresher-practice-finished/src/components/Cart/CartItem.module.css new file mode 100644 index 0000000000..e34100d841 --- /dev/null +++ b/code/02-refresher-practice-finished/src/components/Cart/CartItem.module.css @@ -0,0 +1,58 @@ +.item { + margin: 1rem 0; + background-color: #575757; + padding: 1rem; +} + +.item h3 { + margin: 0 0 0.5rem 0; + font-size: 1.75rem; +} + +.item header { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.details { + display: flex; + justify-content: space-between; + align-items: center; +} + +.quantity span { + font-size: 1.5rem; + font-weight: bold; +} + +.price { + font-size: 1.5rem; + font-weight: bold; +} + +.itemprice { + font-weight: normal; + font-size: 1rem; + font-style: italic; +} + +.actions { + display: flex; + justify-content: flex-end; + margin: 0.5rem 0; +} + +.actions button { + background-color: transparent; + border: 1px solid white; + margin-left: 0.5rem; + padding: 0.15rem 1rem; + color: white; +} + +.actions button:hover, +.actions button:active { + background-color: #4b4b4b; + color: white; +} \ No newline at end of file diff --git a/code/02-refresher-practice-finished/src/components/Layout/Layout.js b/code/02-refresher-practice-finished/src/components/Layout/Layout.js new file mode 100644 index 0000000000..0ce12a011f --- /dev/null +++ b/code/02-refresher-practice-finished/src/components/Layout/Layout.js @@ -0,0 +1,13 @@ +import { Fragment } from 'react'; +import MainHeader from './MainHeader'; + +const Layout = (props) => { + return ( + + +
    {props.children}
    +
    + ); +}; + +export default Layout; diff --git a/code/02-refresher-practice-finished/src/components/Layout/MainHeader.js b/code/02-refresher-practice-finished/src/components/Layout/MainHeader.js new file mode 100644 index 0000000000..38ea37a29b --- /dev/null +++ b/code/02-refresher-practice-finished/src/components/Layout/MainHeader.js @@ -0,0 +1,19 @@ +import CartButton from '../Cart/CartButton'; +import classes from './MainHeader.module.css'; + +const MainHeader = (props) => { + return ( +
    +

    ReduxCart

    + +
    + ); +}; + +export default MainHeader; diff --git a/code/02-refresher-practice-finished/src/components/Layout/MainHeader.module.css b/code/02-refresher-practice-finished/src/components/Layout/MainHeader.module.css new file mode 100644 index 0000000000..e41c98d247 --- /dev/null +++ b/code/02-refresher-practice-finished/src/components/Layout/MainHeader.module.css @@ -0,0 +1,19 @@ +.header { + width: 100%; + height: 5rem; + padding: 0 10%; + display: flex; + align-items: center; + justify-content: space-between; + background-color: #252424; +} + +.header h1 { + color: white; +} + +.header ul { + list-style: none; + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/code/02-refresher-practice-finished/src/components/Shop/ProductItem.js b/code/02-refresher-practice-finished/src/components/Shop/ProductItem.js new file mode 100644 index 0000000000..b60369769b --- /dev/null +++ b/code/02-refresher-practice-finished/src/components/Shop/ProductItem.js @@ -0,0 +1,38 @@ +import { useDispatch } from 'react-redux'; + +import { cartActions } from '../../store/cart-slice'; +import Card from '../UI/Card'; +import classes from './ProductItem.module.css'; + +const ProductItem = (props) => { + const dispatch = useDispatch(); + + const { title, price, description, id } = props; + + const addToCartHandler = () => { + dispatch( + cartActions.addItemToCart({ + id, + title, + price, + }) + ); + }; + + return ( +
  • + +
    +

    {title}

    +
    ${price.toFixed(2)}
    +
    +

    {description}

    +
    + +
    +
    +
  • + ); +}; + +export default ProductItem; diff --git a/code/02-refresher-practice-finished/src/components/Shop/ProductItem.module.css b/code/02-refresher-practice-finished/src/components/Shop/ProductItem.module.css new file mode 100644 index 0000000000..2fcdc5d8a6 --- /dev/null +++ b/code/02-refresher-practice-finished/src/components/Shop/ProductItem.module.css @@ -0,0 +1,27 @@ +.item h3 { + margin: 0.5rem 0; + font-size: 1.25rem; +} + +.item header { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.price { + border-radius: 30px; + padding: 0.15rem 1.5rem; + background-color: #3a3a3a; + color: white; + font-size: 1.5rem; +} + +.item p { + color: #3a3a3a; +} + +.actions { + display: flex; + justify-content: flex-end; +} \ No newline at end of file diff --git a/code/02-refresher-practice-finished/src/components/Shop/Products.js b/code/02-refresher-practice-finished/src/components/Shop/Products.js new file mode 100644 index 0000000000..6b430cc262 --- /dev/null +++ b/code/02-refresher-practice-finished/src/components/Shop/Products.js @@ -0,0 +1,38 @@ +import ProductItem from './ProductItem'; +import classes from './Products.module.css'; + +const DUMMY_PRODUCTS = [ + { + id: 'p1', + price: 6, + title: 'My First Book', + description: 'The first book I ever wrote', + }, + { + id: 'p2', + price: 5, + title: 'My Second Book', + description: 'The second book I ever wrote', + }, +]; + +const Products = (props) => { + return ( +
    +

    Buy your favorite products

    + +
    + ); +}; + +export default Products; diff --git a/code/02-refresher-practice-finished/src/components/Shop/Products.module.css b/code/02-refresher-practice-finished/src/components/Shop/Products.module.css new file mode 100644 index 0000000000..d81c97330f --- /dev/null +++ b/code/02-refresher-practice-finished/src/components/Shop/Products.module.css @@ -0,0 +1,12 @@ +.products h2 { + color: white; + margin: 2rem auto; + text-align: center; + text-transform: uppercase; +} + +.products ul { + list-style: none; + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/code/02-refresher-practice-finished/src/components/UI/Card.js b/code/02-refresher-practice-finished/src/components/UI/Card.js new file mode 100644 index 0000000000..849202f21c --- /dev/null +++ b/code/02-refresher-practice-finished/src/components/UI/Card.js @@ -0,0 +1,13 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return ( +
    + {props.children} +
    + ); +}; + +export default Card; diff --git a/code/02-refresher-practice-finished/src/components/UI/Card.module.css b/code/02-refresher-practice-finished/src/components/UI/Card.module.css new file mode 100644 index 0000000000..ac9c6709f4 --- /dev/null +++ b/code/02-refresher-practice-finished/src/components/UI/Card.module.css @@ -0,0 +1,8 @@ +.card { + margin: 1rem auto; + border-radius: 6px; + background-color: white; + padding: 1rem; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/02-refresher-practice-finished/src/index.css b/code/02-refresher-practice-finished/src/index.css new file mode 100644 index 0000000000..3431f5f884 --- /dev/null +++ b/code/02-refresher-practice-finished/src/index.css @@ -0,0 +1,31 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'); + +* { + box-sizing: border-box; +} + +html { + font-family: 'Noto Sans JP', sans-serif; +} + +body { + margin: 0; + background-color: #3f3f3f; +} + +button { + font: inherit; + cursor: pointer; + padding: 0.5rem 1.5rem; + border-radius: 6px; + background-color: transparent; + color: #1a8ed1; + border: 1px solid #1a8ed1; +} + +button:hover, +button:active { + background-color: #1ac5d1; + border-color: #1ac5d1; + color: white; +} \ No newline at end of file diff --git a/code/02-refresher-practice-finished/src/index.js b/code/02-refresher-practice-finished/src/index.js new file mode 100644 index 0000000000..c67c8acc68 --- /dev/null +++ b/code/02-refresher-practice-finished/src/index.js @@ -0,0 +1,13 @@ +import ReactDOM from 'react-dom/client'; +import { Provider } from 'react-redux'; + +import store from './store/index'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/02-refresher-practice-finished/src/store/cart-slice.js b/code/02-refresher-practice-finished/src/store/cart-slice.js new file mode 100644 index 0000000000..dce632d86c --- /dev/null +++ b/code/02-refresher-practice-finished/src/store/cart-slice.js @@ -0,0 +1,42 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const cartSlice = createSlice({ + name: 'cart', + initialState: { + items: [], + totalQuantity: 0, + }, + reducers: { + addItemToCart(state, action) { + const newItem = action.payload; + const existingItem = state.items.find((item) => item.id === newItem.id); + state.totalQuantity++; + if (!existingItem) { + state.items.push({ + id: newItem.id, + price: newItem.price, + quantity: 1, + totalPrice: newItem.price, + name: newItem.title + }); + } else { + existingItem.quantity++; + existingItem.totalPrice = existingItem.totalPrice + newItem.price; + } + }, + removeItemFromCart(state, action) { + const id = action.payload; + const existingItem = state.items.find(item => item.id === id); + state.totalQuantity--; + if (existingItem.quantity === 1) { + state.items = state.items.filter(item => item.id !== id); + } else { + existingItem.quantity--; + } + }, + }, +}); + +export const cartActions = cartSlice.actions; + +export default cartSlice; \ No newline at end of file diff --git a/code/02-refresher-practice-finished/src/store/index.js b/code/02-refresher-practice-finished/src/store/index.js new file mode 100644 index 0000000000..d663f26de2 --- /dev/null +++ b/code/02-refresher-practice-finished/src/store/index.js @@ -0,0 +1,10 @@ +import { configureStore } from '@reduxjs/toolkit'; + +import uiSlice from './ui-slice'; +import cartSlice from './cart-slice'; + +const store = configureStore({ + reducer: { ui: uiSlice.reducer, cart: cartSlice.reducer }, +}); + +export default store; diff --git a/code/02-refresher-practice-finished/src/store/ui-slice.js b/code/02-refresher-practice-finished/src/store/ui-slice.js new file mode 100644 index 0000000000..4fa43c1b12 --- /dev/null +++ b/code/02-refresher-practice-finished/src/store/ui-slice.js @@ -0,0 +1,15 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const uiSlice = createSlice({ + name: 'ui', + initialState: { cartIsVisible: false }, + reducers: { + toggle(state) { + state.cartIsVisible = !state.cartIsVisible; + } + } +}); + +export const uiActions = uiSlice.actions; + +export default uiSlice; \ No newline at end of file diff --git a/code/03-using-useeffect-with-redux/package.json b/code/03-using-useeffect-with-redux/package.json new file mode 100644 index 0000000000..99c15d93ce --- /dev/null +++ b/code/03-using-useeffect-with-redux/package.json @@ -0,0 +1,40 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@reduxjs/toolkit": "^1.5.0", + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-redux": "^7.2.2", + "react-scripts": "4.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/03-using-useeffect-with-redux/public/favicon.ico b/code/03-using-useeffect-with-redux/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/03-using-useeffect-with-redux/public/favicon.ico differ diff --git a/code/03-using-useeffect-with-redux/public/index.html b/code/03-using-useeffect-with-redux/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/03-using-useeffect-with-redux/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/code/03-using-useeffect-with-redux/public/logo192.png b/code/03-using-useeffect-with-redux/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/03-using-useeffect-with-redux/public/logo192.png differ diff --git a/code/03-using-useeffect-with-redux/public/logo512.png b/code/03-using-useeffect-with-redux/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/03-using-useeffect-with-redux/public/logo512.png differ diff --git a/code/03-using-useeffect-with-redux/public/manifest.json b/code/03-using-useeffect-with-redux/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/03-using-useeffect-with-redux/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/03-using-useeffect-with-redux/public/robots.txt b/code/03-using-useeffect-with-redux/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/03-using-useeffect-with-redux/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/03-using-useeffect-with-redux/src/App.js b/code/03-using-useeffect-with-redux/src/App.js new file mode 100644 index 0000000000..096f522320 --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/App.js @@ -0,0 +1,27 @@ +import { useEffect } from 'react'; +import { useSelector } from 'react-redux'; + +import Cart from './components/Cart/Cart'; +import Layout from './components/Layout/Layout'; +import Products from './components/Shop/Products'; + +function App() { + const showCart = useSelector((state) => state.ui.cartIsVisible); + const cart = useSelector((state) => state.cart); + + useEffect(() => { + fetch('https://react-http-6b4a6.firebaseio.com/cart.json', { + method: 'PUT', + body: JSON.stringify(cart), + }); + }, [cart]); + + return ( + + {showCart && } + + + ); +} + +export default App; diff --git a/code/03-using-useeffect-with-redux/src/components/Cart/Cart.js b/code/03-using-useeffect-with-redux/src/components/Cart/Cart.js new file mode 100644 index 0000000000..33030d2089 --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/components/Cart/Cart.js @@ -0,0 +1,31 @@ +import { useSelector } from 'react-redux'; + +import Card from '../UI/Card'; +import classes from './Cart.module.css'; +import CartItem from './CartItem'; + +const Cart = (props) => { + const cartItems = useSelector((state) => state.cart.items); + + return ( + +

    Your Shopping Cart

    + +
    + ); +}; + +export default Cart; diff --git a/code/03-using-useeffect-with-redux/src/components/Cart/Cart.module.css b/code/03-using-useeffect-with-redux/src/components/Cart/Cart.module.css new file mode 100644 index 0000000000..95670ab70b --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/components/Cart/Cart.module.css @@ -0,0 +1,16 @@ +.cart { + max-width: 30rem; + background-color: #313131; + color: white; +} + +.cart h2 { + font-size: 1.25rem; + margin: 0.5rem 0; +} + +.cart ul { + list-style: none; + padding: 0; + margin: 0; +} \ No newline at end of file diff --git a/code/03-using-useeffect-with-redux/src/components/Cart/CartButton.js b/code/03-using-useeffect-with-redux/src/components/Cart/CartButton.js new file mode 100644 index 0000000000..4e59baac3c --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/components/Cart/CartButton.js @@ -0,0 +1,22 @@ +import { useDispatch, useSelector } from 'react-redux'; + +import { uiActions } from '../../store/ui-slice'; +import classes from './CartButton.module.css'; + +const CartButton = (props) => { + const dispatch = useDispatch(); + const cartQuantity = useSelector((state) => state.cart.totalQuantity); + + const toggleCartHandler = () => { + dispatch(uiActions.toggle()); + }; + + return ( + + ); +}; + +export default CartButton; diff --git a/code/03-using-useeffect-with-redux/src/components/Cart/CartButton.module.css b/code/03-using-useeffect-with-redux/src/components/Cart/CartButton.module.css new file mode 100644 index 0000000000..93445f4596 --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/components/Cart/CartButton.module.css @@ -0,0 +1,21 @@ +.button { + background-color: transparent; + border-color: #1ad1b9; + color: #1ad1b9; +} + +.button:hover, +.button:active { + color: white; +} + +.button span { + margin: 0 0.5rem; +} + +.badge { + background-color: #1ad1b9; + border-radius: 30px; + padding: 0.15rem 1.25rem; + color: #1d1d1d; +} \ No newline at end of file diff --git a/code/03-using-useeffect-with-redux/src/components/Cart/CartItem.js b/code/03-using-useeffect-with-redux/src/components/Cart/CartItem.js new file mode 100644 index 0000000000..e1c6a8f012 --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/components/Cart/CartItem.js @@ -0,0 +1,47 @@ +import { useDispatch } from 'react-redux'; + +import classes from './CartItem.module.css'; +import { cartActions } from '../../store/cart-slice'; + +const CartItem = (props) => { + const dispatch = useDispatch(); + + const { title, quantity, total, price, id } = props.item; + + const removeItemHandler = () => { + dispatch(cartActions.removeItemFromCart(id)); + }; + + const addItemHandler = () => { + dispatch( + cartActions.addItemToCart({ + id, + title, + price, + }) + ); + }; + + return ( +
  • +
    +

    {title}

    +
    + ${total.toFixed(2)}{' '} + (${price.toFixed(2)}/item) +
    +
    +
    +
    + x {quantity} +
    +
    + + +
    +
    +
  • + ); +}; + +export default CartItem; diff --git a/code/03-using-useeffect-with-redux/src/components/Cart/CartItem.module.css b/code/03-using-useeffect-with-redux/src/components/Cart/CartItem.module.css new file mode 100644 index 0000000000..e34100d841 --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/components/Cart/CartItem.module.css @@ -0,0 +1,58 @@ +.item { + margin: 1rem 0; + background-color: #575757; + padding: 1rem; +} + +.item h3 { + margin: 0 0 0.5rem 0; + font-size: 1.75rem; +} + +.item header { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.details { + display: flex; + justify-content: space-between; + align-items: center; +} + +.quantity span { + font-size: 1.5rem; + font-weight: bold; +} + +.price { + font-size: 1.5rem; + font-weight: bold; +} + +.itemprice { + font-weight: normal; + font-size: 1rem; + font-style: italic; +} + +.actions { + display: flex; + justify-content: flex-end; + margin: 0.5rem 0; +} + +.actions button { + background-color: transparent; + border: 1px solid white; + margin-left: 0.5rem; + padding: 0.15rem 1rem; + color: white; +} + +.actions button:hover, +.actions button:active { + background-color: #4b4b4b; + color: white; +} \ No newline at end of file diff --git a/code/03-using-useeffect-with-redux/src/components/Layout/Layout.js b/code/03-using-useeffect-with-redux/src/components/Layout/Layout.js new file mode 100644 index 0000000000..0ce12a011f --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/components/Layout/Layout.js @@ -0,0 +1,13 @@ +import { Fragment } from 'react'; +import MainHeader from './MainHeader'; + +const Layout = (props) => { + return ( + + +
    {props.children}
    +
    + ); +}; + +export default Layout; diff --git a/code/03-using-useeffect-with-redux/src/components/Layout/MainHeader.js b/code/03-using-useeffect-with-redux/src/components/Layout/MainHeader.js new file mode 100644 index 0000000000..38ea37a29b --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/components/Layout/MainHeader.js @@ -0,0 +1,19 @@ +import CartButton from '../Cart/CartButton'; +import classes from './MainHeader.module.css'; + +const MainHeader = (props) => { + return ( +
    +

    ReduxCart

    + +
    + ); +}; + +export default MainHeader; diff --git a/code/03-using-useeffect-with-redux/src/components/Layout/MainHeader.module.css b/code/03-using-useeffect-with-redux/src/components/Layout/MainHeader.module.css new file mode 100644 index 0000000000..e41c98d247 --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/components/Layout/MainHeader.module.css @@ -0,0 +1,19 @@ +.header { + width: 100%; + height: 5rem; + padding: 0 10%; + display: flex; + align-items: center; + justify-content: space-between; + background-color: #252424; +} + +.header h1 { + color: white; +} + +.header ul { + list-style: none; + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/code/03-using-useeffect-with-redux/src/components/Shop/ProductItem.js b/code/03-using-useeffect-with-redux/src/components/Shop/ProductItem.js new file mode 100644 index 0000000000..4a2a4b3f9f --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/components/Shop/ProductItem.js @@ -0,0 +1,41 @@ +import { useDispatch } from 'react-redux'; + +import { cartActions } from '../../store/cart-slice'; +import Card from '../UI/Card'; +import classes from './ProductItem.module.css'; + +const ProductItem = (props) => { + const dispatch = useDispatch(); + + const { title, price, description, id } = props; + + const addToCartHandler = () => { + // and then send Http request + // fetch('firebase-url', { method: 'POST', body: JSON.stringify(newCart) }) + + dispatch( + cartActions.addItemToCart({ + id, + title, + price, + }) + ); + }; + + return ( +
  • + +
    +

    {title}

    +
    ${price.toFixed(2)}
    +
    +

    {description}

    +
    + +
    +
    +
  • + ); +}; + +export default ProductItem; diff --git a/code/03-using-useeffect-with-redux/src/components/Shop/ProductItem.module.css b/code/03-using-useeffect-with-redux/src/components/Shop/ProductItem.module.css new file mode 100644 index 0000000000..2fcdc5d8a6 --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/components/Shop/ProductItem.module.css @@ -0,0 +1,27 @@ +.item h3 { + margin: 0.5rem 0; + font-size: 1.25rem; +} + +.item header { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.price { + border-radius: 30px; + padding: 0.15rem 1.5rem; + background-color: #3a3a3a; + color: white; + font-size: 1.5rem; +} + +.item p { + color: #3a3a3a; +} + +.actions { + display: flex; + justify-content: flex-end; +} \ No newline at end of file diff --git a/code/03-using-useeffect-with-redux/src/components/Shop/Products.js b/code/03-using-useeffect-with-redux/src/components/Shop/Products.js new file mode 100644 index 0000000000..6b430cc262 --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/components/Shop/Products.js @@ -0,0 +1,38 @@ +import ProductItem from './ProductItem'; +import classes from './Products.module.css'; + +const DUMMY_PRODUCTS = [ + { + id: 'p1', + price: 6, + title: 'My First Book', + description: 'The first book I ever wrote', + }, + { + id: 'p2', + price: 5, + title: 'My Second Book', + description: 'The second book I ever wrote', + }, +]; + +const Products = (props) => { + return ( +
    +

    Buy your favorite products

    + +
    + ); +}; + +export default Products; diff --git a/code/03-using-useeffect-with-redux/src/components/Shop/Products.module.css b/code/03-using-useeffect-with-redux/src/components/Shop/Products.module.css new file mode 100644 index 0000000000..d81c97330f --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/components/Shop/Products.module.css @@ -0,0 +1,12 @@ +.products h2 { + color: white; + margin: 2rem auto; + text-align: center; + text-transform: uppercase; +} + +.products ul { + list-style: none; + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/code/03-using-useeffect-with-redux/src/components/UI/Card.js b/code/03-using-useeffect-with-redux/src/components/UI/Card.js new file mode 100644 index 0000000000..849202f21c --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/components/UI/Card.js @@ -0,0 +1,13 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return ( +
    + {props.children} +
    + ); +}; + +export default Card; diff --git a/code/03-using-useeffect-with-redux/src/components/UI/Card.module.css b/code/03-using-useeffect-with-redux/src/components/UI/Card.module.css new file mode 100644 index 0000000000..ac9c6709f4 --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/components/UI/Card.module.css @@ -0,0 +1,8 @@ +.card { + margin: 1rem auto; + border-radius: 6px; + background-color: white; + padding: 1rem; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/03-using-useeffect-with-redux/src/index.css b/code/03-using-useeffect-with-redux/src/index.css new file mode 100644 index 0000000000..3431f5f884 --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/index.css @@ -0,0 +1,31 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'); + +* { + box-sizing: border-box; +} + +html { + font-family: 'Noto Sans JP', sans-serif; +} + +body { + margin: 0; + background-color: #3f3f3f; +} + +button { + font: inherit; + cursor: pointer; + padding: 0.5rem 1.5rem; + border-radius: 6px; + background-color: transparent; + color: #1a8ed1; + border: 1px solid #1a8ed1; +} + +button:hover, +button:active { + background-color: #1ac5d1; + border-color: #1ac5d1; + color: white; +} \ No newline at end of file diff --git a/code/03-using-useeffect-with-redux/src/index.js b/code/03-using-useeffect-with-redux/src/index.js new file mode 100644 index 0000000000..c67c8acc68 --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/index.js @@ -0,0 +1,13 @@ +import ReactDOM from 'react-dom/client'; +import { Provider } from 'react-redux'; + +import store from './store/index'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/03-using-useeffect-with-redux/src/store/cart-slice.js b/code/03-using-useeffect-with-redux/src/store/cart-slice.js new file mode 100644 index 0000000000..e629fc66ce --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/store/cart-slice.js @@ -0,0 +1,46 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const cartSlice = createSlice({ + name: 'cart', + initialState: { + items: [], + totalQuantity: 0, + }, + reducers: { + replaceCart(state, action) { + state.totalQuantity = action.payload.totalQuantity; + state.items = action.payload.items; + }, + addItemToCart(state, action) { + const newItem = action.payload; + const existingItem = state.items.find((item) => item.id === newItem.id); + state.totalQuantity++; + if (!existingItem) { + state.items.push({ + id: newItem.id, + price: newItem.price, + quantity: 1, + totalPrice: newItem.price, + name: newItem.title, + }); + } else { + existingItem.quantity++; + existingItem.totalPrice = existingItem.totalPrice + newItem.price; + } + }, + removeItemFromCart(state, action) { + const id = action.payload; + const existingItem = state.items.find((item) => item.id === id); + state.totalQuantity--; + if (existingItem.quantity === 1) { + state.items = state.items.filter((item) => item.id !== id); + } else { + existingItem.quantity--; + } + }, + }, +}); + +export const cartActions = cartSlice.actions; + +export default cartSlice; diff --git a/code/03-using-useeffect-with-redux/src/store/index.js b/code/03-using-useeffect-with-redux/src/store/index.js new file mode 100644 index 0000000000..d663f26de2 --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/store/index.js @@ -0,0 +1,10 @@ +import { configureStore } from '@reduxjs/toolkit'; + +import uiSlice from './ui-slice'; +import cartSlice from './cart-slice'; + +const store = configureStore({ + reducer: { ui: uiSlice.reducer, cart: cartSlice.reducer }, +}); + +export default store; diff --git a/code/03-using-useeffect-with-redux/src/store/ui-slice.js b/code/03-using-useeffect-with-redux/src/store/ui-slice.js new file mode 100644 index 0000000000..4fa43c1b12 --- /dev/null +++ b/code/03-using-useeffect-with-redux/src/store/ui-slice.js @@ -0,0 +1,15 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const uiSlice = createSlice({ + name: 'ui', + initialState: { cartIsVisible: false }, + reducers: { + toggle(state) { + state.cartIsVisible = !state.cartIsVisible; + } + } +}); + +export const uiActions = uiSlice.actions; + +export default uiSlice; \ No newline at end of file diff --git a/code/04-handling-http-states-feedback-with-redux/package.json b/code/04-handling-http-states-feedback-with-redux/package.json new file mode 100644 index 0000000000..99c15d93ce --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/package.json @@ -0,0 +1,40 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@reduxjs/toolkit": "^1.5.0", + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-redux": "^7.2.2", + "react-scripts": "4.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/04-handling-http-states-feedback-with-redux/public/favicon.ico b/code/04-handling-http-states-feedback-with-redux/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/04-handling-http-states-feedback-with-redux/public/favicon.ico differ diff --git a/code/04-handling-http-states-feedback-with-redux/public/index.html b/code/04-handling-http-states-feedback-with-redux/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/code/04-handling-http-states-feedback-with-redux/public/logo192.png b/code/04-handling-http-states-feedback-with-redux/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/04-handling-http-states-feedback-with-redux/public/logo192.png differ diff --git a/code/04-handling-http-states-feedback-with-redux/public/logo512.png b/code/04-handling-http-states-feedback-with-redux/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/04-handling-http-states-feedback-with-redux/public/logo512.png differ diff --git a/code/04-handling-http-states-feedback-with-redux/public/manifest.json b/code/04-handling-http-states-feedback-with-redux/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/04-handling-http-states-feedback-with-redux/public/robots.txt b/code/04-handling-http-states-feedback-with-redux/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/04-handling-http-states-feedback-with-redux/src/App.js b/code/04-handling-http-states-feedback-with-redux/src/App.js new file mode 100644 index 0000000000..3778b223a1 --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/App.js @@ -0,0 +1,81 @@ +import { Fragment, useEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; + +import Cart from './components/Cart/Cart'; +import Layout from './components/Layout/Layout'; +import Products from './components/Shop/Products'; +import { uiActions } from './store/ui-slice'; +import Notification from './components/UI/Notification'; + +let isInitial = true; + +function App() { + const dispatch = useDispatch(); + const showCart = useSelector((state) => state.ui.cartIsVisible); + const cart = useSelector((state) => state.cart); + const notification = useSelector((state) => state.ui.notification); + + useEffect(() => { + const sendCartData = async () => { + dispatch( + uiActions.showNotification({ + status: 'pending', + title: 'Sending...', + message: 'Sending cart data!', + }) + ); + const response = await fetch( + 'https://react-http-6b4a6.firebaseio.com/cart.json', + { + method: 'PUT', + body: JSON.stringify(cart), + } + ); + + if (!response.ok) { + throw new Error('Sending cart data failed.'); + } + + dispatch( + uiActions.showNotification({ + status: 'success', + title: 'Success!', + message: 'Sent cart data successfully!', + }) + ); + }; + + if (isInitial) { + isInitial = false; + return; + } + + sendCartData().catch((error) => { + dispatch( + uiActions.showNotification({ + status: 'error', + title: 'Error!', + message: 'Sending cart data failed!', + }) + ); + }); + }, [cart, dispatch]); + + return ( + + {notification && ( + + )} + + {showCart && } + + + + ); +} + +export default App; diff --git a/code/04-handling-http-states-feedback-with-redux/src/components/Cart/Cart.js b/code/04-handling-http-states-feedback-with-redux/src/components/Cart/Cart.js new file mode 100644 index 0000000000..33030d2089 --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/components/Cart/Cart.js @@ -0,0 +1,31 @@ +import { useSelector } from 'react-redux'; + +import Card from '../UI/Card'; +import classes from './Cart.module.css'; +import CartItem from './CartItem'; + +const Cart = (props) => { + const cartItems = useSelector((state) => state.cart.items); + + return ( + +

    Your Shopping Cart

    + +
    + ); +}; + +export default Cart; diff --git a/code/04-handling-http-states-feedback-with-redux/src/components/Cart/Cart.module.css b/code/04-handling-http-states-feedback-with-redux/src/components/Cart/Cart.module.css new file mode 100644 index 0000000000..95670ab70b --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/components/Cart/Cart.module.css @@ -0,0 +1,16 @@ +.cart { + max-width: 30rem; + background-color: #313131; + color: white; +} + +.cart h2 { + font-size: 1.25rem; + margin: 0.5rem 0; +} + +.cart ul { + list-style: none; + padding: 0; + margin: 0; +} \ No newline at end of file diff --git a/code/04-handling-http-states-feedback-with-redux/src/components/Cart/CartButton.js b/code/04-handling-http-states-feedback-with-redux/src/components/Cart/CartButton.js new file mode 100644 index 0000000000..4e59baac3c --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/components/Cart/CartButton.js @@ -0,0 +1,22 @@ +import { useDispatch, useSelector } from 'react-redux'; + +import { uiActions } from '../../store/ui-slice'; +import classes from './CartButton.module.css'; + +const CartButton = (props) => { + const dispatch = useDispatch(); + const cartQuantity = useSelector((state) => state.cart.totalQuantity); + + const toggleCartHandler = () => { + dispatch(uiActions.toggle()); + }; + + return ( + + ); +}; + +export default CartButton; diff --git a/code/04-handling-http-states-feedback-with-redux/src/components/Cart/CartButton.module.css b/code/04-handling-http-states-feedback-with-redux/src/components/Cart/CartButton.module.css new file mode 100644 index 0000000000..93445f4596 --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/components/Cart/CartButton.module.css @@ -0,0 +1,21 @@ +.button { + background-color: transparent; + border-color: #1ad1b9; + color: #1ad1b9; +} + +.button:hover, +.button:active { + color: white; +} + +.button span { + margin: 0 0.5rem; +} + +.badge { + background-color: #1ad1b9; + border-radius: 30px; + padding: 0.15rem 1.25rem; + color: #1d1d1d; +} \ No newline at end of file diff --git a/code/04-handling-http-states-feedback-with-redux/src/components/Cart/CartItem.js b/code/04-handling-http-states-feedback-with-redux/src/components/Cart/CartItem.js new file mode 100644 index 0000000000..e1c6a8f012 --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/components/Cart/CartItem.js @@ -0,0 +1,47 @@ +import { useDispatch } from 'react-redux'; + +import classes from './CartItem.module.css'; +import { cartActions } from '../../store/cart-slice'; + +const CartItem = (props) => { + const dispatch = useDispatch(); + + const { title, quantity, total, price, id } = props.item; + + const removeItemHandler = () => { + dispatch(cartActions.removeItemFromCart(id)); + }; + + const addItemHandler = () => { + dispatch( + cartActions.addItemToCart({ + id, + title, + price, + }) + ); + }; + + return ( +
  • +
    +

    {title}

    +
    + ${total.toFixed(2)}{' '} + (${price.toFixed(2)}/item) +
    +
    +
    +
    + x {quantity} +
    +
    + + +
    +
    +
  • + ); +}; + +export default CartItem; diff --git a/code/04-handling-http-states-feedback-with-redux/src/components/Cart/CartItem.module.css b/code/04-handling-http-states-feedback-with-redux/src/components/Cart/CartItem.module.css new file mode 100644 index 0000000000..e34100d841 --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/components/Cart/CartItem.module.css @@ -0,0 +1,58 @@ +.item { + margin: 1rem 0; + background-color: #575757; + padding: 1rem; +} + +.item h3 { + margin: 0 0 0.5rem 0; + font-size: 1.75rem; +} + +.item header { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.details { + display: flex; + justify-content: space-between; + align-items: center; +} + +.quantity span { + font-size: 1.5rem; + font-weight: bold; +} + +.price { + font-size: 1.5rem; + font-weight: bold; +} + +.itemprice { + font-weight: normal; + font-size: 1rem; + font-style: italic; +} + +.actions { + display: flex; + justify-content: flex-end; + margin: 0.5rem 0; +} + +.actions button { + background-color: transparent; + border: 1px solid white; + margin-left: 0.5rem; + padding: 0.15rem 1rem; + color: white; +} + +.actions button:hover, +.actions button:active { + background-color: #4b4b4b; + color: white; +} \ No newline at end of file diff --git a/code/04-handling-http-states-feedback-with-redux/src/components/Layout/Layout.js b/code/04-handling-http-states-feedback-with-redux/src/components/Layout/Layout.js new file mode 100644 index 0000000000..0ce12a011f --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/components/Layout/Layout.js @@ -0,0 +1,13 @@ +import { Fragment } from 'react'; +import MainHeader from './MainHeader'; + +const Layout = (props) => { + return ( + + +
    {props.children}
    +
    + ); +}; + +export default Layout; diff --git a/code/04-handling-http-states-feedback-with-redux/src/components/Layout/MainHeader.js b/code/04-handling-http-states-feedback-with-redux/src/components/Layout/MainHeader.js new file mode 100644 index 0000000000..38ea37a29b --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/components/Layout/MainHeader.js @@ -0,0 +1,19 @@ +import CartButton from '../Cart/CartButton'; +import classes from './MainHeader.module.css'; + +const MainHeader = (props) => { + return ( +
    +

    ReduxCart

    + +
    + ); +}; + +export default MainHeader; diff --git a/code/04-handling-http-states-feedback-with-redux/src/components/Layout/MainHeader.module.css b/code/04-handling-http-states-feedback-with-redux/src/components/Layout/MainHeader.module.css new file mode 100644 index 0000000000..e41c98d247 --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/components/Layout/MainHeader.module.css @@ -0,0 +1,19 @@ +.header { + width: 100%; + height: 5rem; + padding: 0 10%; + display: flex; + align-items: center; + justify-content: space-between; + background-color: #252424; +} + +.header h1 { + color: white; +} + +.header ul { + list-style: none; + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/code/04-handling-http-states-feedback-with-redux/src/components/Shop/ProductItem.js b/code/04-handling-http-states-feedback-with-redux/src/components/Shop/ProductItem.js new file mode 100644 index 0000000000..4a2a4b3f9f --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/components/Shop/ProductItem.js @@ -0,0 +1,41 @@ +import { useDispatch } from 'react-redux'; + +import { cartActions } from '../../store/cart-slice'; +import Card from '../UI/Card'; +import classes from './ProductItem.module.css'; + +const ProductItem = (props) => { + const dispatch = useDispatch(); + + const { title, price, description, id } = props; + + const addToCartHandler = () => { + // and then send Http request + // fetch('firebase-url', { method: 'POST', body: JSON.stringify(newCart) }) + + dispatch( + cartActions.addItemToCart({ + id, + title, + price, + }) + ); + }; + + return ( +
  • + +
    +

    {title}

    +
    ${price.toFixed(2)}
    +
    +

    {description}

    +
    + +
    +
    +
  • + ); +}; + +export default ProductItem; diff --git a/code/04-handling-http-states-feedback-with-redux/src/components/Shop/ProductItem.module.css b/code/04-handling-http-states-feedback-with-redux/src/components/Shop/ProductItem.module.css new file mode 100644 index 0000000000..2fcdc5d8a6 --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/components/Shop/ProductItem.module.css @@ -0,0 +1,27 @@ +.item h3 { + margin: 0.5rem 0; + font-size: 1.25rem; +} + +.item header { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.price { + border-radius: 30px; + padding: 0.15rem 1.5rem; + background-color: #3a3a3a; + color: white; + font-size: 1.5rem; +} + +.item p { + color: #3a3a3a; +} + +.actions { + display: flex; + justify-content: flex-end; +} \ No newline at end of file diff --git a/code/04-handling-http-states-feedback-with-redux/src/components/Shop/Products.js b/code/04-handling-http-states-feedback-with-redux/src/components/Shop/Products.js new file mode 100644 index 0000000000..6b430cc262 --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/components/Shop/Products.js @@ -0,0 +1,38 @@ +import ProductItem from './ProductItem'; +import classes from './Products.module.css'; + +const DUMMY_PRODUCTS = [ + { + id: 'p1', + price: 6, + title: 'My First Book', + description: 'The first book I ever wrote', + }, + { + id: 'p2', + price: 5, + title: 'My Second Book', + description: 'The second book I ever wrote', + }, +]; + +const Products = (props) => { + return ( +
    +

    Buy your favorite products

    + +
    + ); +}; + +export default Products; diff --git a/code/04-handling-http-states-feedback-with-redux/src/components/Shop/Products.module.css b/code/04-handling-http-states-feedback-with-redux/src/components/Shop/Products.module.css new file mode 100644 index 0000000000..d81c97330f --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/components/Shop/Products.module.css @@ -0,0 +1,12 @@ +.products h2 { + color: white; + margin: 2rem auto; + text-align: center; + text-transform: uppercase; +} + +.products ul { + list-style: none; + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/code/04-handling-http-states-feedback-with-redux/src/components/UI/Card.js b/code/04-handling-http-states-feedback-with-redux/src/components/UI/Card.js new file mode 100644 index 0000000000..849202f21c --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/components/UI/Card.js @@ -0,0 +1,13 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return ( +
    + {props.children} +
    + ); +}; + +export default Card; diff --git a/code/04-handling-http-states-feedback-with-redux/src/components/UI/Card.module.css b/code/04-handling-http-states-feedback-with-redux/src/components/UI/Card.module.css new file mode 100644 index 0000000000..ac9c6709f4 --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/components/UI/Card.module.css @@ -0,0 +1,8 @@ +.card { + margin: 1rem auto; + border-radius: 6px; + background-color: white; + padding: 1rem; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/04-handling-http-states-feedback-with-redux/src/components/UI/Notification.js b/code/04-handling-http-states-feedback-with-redux/src/components/UI/Notification.js new file mode 100644 index 0000000000..982017fa74 --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/components/UI/Notification.js @@ -0,0 +1,23 @@ +import classes from './Notification.module.css'; + +const Notification = (props) => { + let specialClasses = ''; + + if (props.status === 'error') { + specialClasses = classes.error; + } + if (props.status === 'success') { + specialClasses = classes.success; + } + + const cssClasses = `${classes.notification} ${specialClasses}`; + + return ( +
    +

    {props.title}

    +

    {props.message}

    +
    + ); +}; + +export default Notification; diff --git a/code/04-handling-http-states-feedback-with-redux/src/components/UI/Notification.module.css b/code/04-handling-http-states-feedback-with-redux/src/components/UI/Notification.module.css new file mode 100644 index 0000000000..5decd98ea5 --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/components/UI/Notification.module.css @@ -0,0 +1,24 @@ +.notification { + width: 100%; + height: 3rem; + background-color: #1a8ed1; + display: flex; + justify-content: space-between; + padding: 0.5rem 10%; + align-items: center; + color: white; +} + +.notification h2, +.notification p { + font-size: 1rem; + margin: 0; +} + +.error { + background-color: #690000; +} + +.success { + background-color: #1ad1b9; +} \ No newline at end of file diff --git a/code/04-handling-http-states-feedback-with-redux/src/index.css b/code/04-handling-http-states-feedback-with-redux/src/index.css new file mode 100644 index 0000000000..3431f5f884 --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/index.css @@ -0,0 +1,31 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'); + +* { + box-sizing: border-box; +} + +html { + font-family: 'Noto Sans JP', sans-serif; +} + +body { + margin: 0; + background-color: #3f3f3f; +} + +button { + font: inherit; + cursor: pointer; + padding: 0.5rem 1.5rem; + border-radius: 6px; + background-color: transparent; + color: #1a8ed1; + border: 1px solid #1a8ed1; +} + +button:hover, +button:active { + background-color: #1ac5d1; + border-color: #1ac5d1; + color: white; +} \ No newline at end of file diff --git a/code/04-handling-http-states-feedback-with-redux/src/index.js b/code/04-handling-http-states-feedback-with-redux/src/index.js new file mode 100644 index 0000000000..c67c8acc68 --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/index.js @@ -0,0 +1,13 @@ +import ReactDOM from 'react-dom/client'; +import { Provider } from 'react-redux'; + +import store from './store/index'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/04-handling-http-states-feedback-with-redux/src/store/cart-slice.js b/code/04-handling-http-states-feedback-with-redux/src/store/cart-slice.js new file mode 100644 index 0000000000..e629fc66ce --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/store/cart-slice.js @@ -0,0 +1,46 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const cartSlice = createSlice({ + name: 'cart', + initialState: { + items: [], + totalQuantity: 0, + }, + reducers: { + replaceCart(state, action) { + state.totalQuantity = action.payload.totalQuantity; + state.items = action.payload.items; + }, + addItemToCart(state, action) { + const newItem = action.payload; + const existingItem = state.items.find((item) => item.id === newItem.id); + state.totalQuantity++; + if (!existingItem) { + state.items.push({ + id: newItem.id, + price: newItem.price, + quantity: 1, + totalPrice: newItem.price, + name: newItem.title, + }); + } else { + existingItem.quantity++; + existingItem.totalPrice = existingItem.totalPrice + newItem.price; + } + }, + removeItemFromCart(state, action) { + const id = action.payload; + const existingItem = state.items.find((item) => item.id === id); + state.totalQuantity--; + if (existingItem.quantity === 1) { + state.items = state.items.filter((item) => item.id !== id); + } else { + existingItem.quantity--; + } + }, + }, +}); + +export const cartActions = cartSlice.actions; + +export default cartSlice; diff --git a/code/04-handling-http-states-feedback-with-redux/src/store/index.js b/code/04-handling-http-states-feedback-with-redux/src/store/index.js new file mode 100644 index 0000000000..d663f26de2 --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/store/index.js @@ -0,0 +1,10 @@ +import { configureStore } from '@reduxjs/toolkit'; + +import uiSlice from './ui-slice'; +import cartSlice from './cart-slice'; + +const store = configureStore({ + reducer: { ui: uiSlice.reducer, cart: cartSlice.reducer }, +}); + +export default store; diff --git a/code/04-handling-http-states-feedback-with-redux/src/store/ui-slice.js b/code/04-handling-http-states-feedback-with-redux/src/store/ui-slice.js new file mode 100644 index 0000000000..a25529a6f0 --- /dev/null +++ b/code/04-handling-http-states-feedback-with-redux/src/store/ui-slice.js @@ -0,0 +1,22 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const uiSlice = createSlice({ + name: 'ui', + initialState: { cartIsVisible: false, notification: null }, + reducers: { + toggle(state) { + state.cartIsVisible = !state.cartIsVisible; + }, + showNotification(state, action) { + state.notification = { + status: action.payload.status, + title: action.payload.title, + message: action.payload.message, + }; + }, + }, +}); + +export const uiActions = uiSlice.actions; + +export default uiSlice; diff --git a/code/05-using-an-action-creator-thunk/package.json b/code/05-using-an-action-creator-thunk/package.json new file mode 100644 index 0000000000..99c15d93ce --- /dev/null +++ b/code/05-using-an-action-creator-thunk/package.json @@ -0,0 +1,40 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@reduxjs/toolkit": "^1.5.0", + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-redux": "^7.2.2", + "react-scripts": "4.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/05-using-an-action-creator-thunk/public/favicon.ico b/code/05-using-an-action-creator-thunk/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/05-using-an-action-creator-thunk/public/favicon.ico differ diff --git a/code/05-using-an-action-creator-thunk/public/index.html b/code/05-using-an-action-creator-thunk/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/05-using-an-action-creator-thunk/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/code/05-using-an-action-creator-thunk/public/logo192.png b/code/05-using-an-action-creator-thunk/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/05-using-an-action-creator-thunk/public/logo192.png differ diff --git a/code/05-using-an-action-creator-thunk/public/logo512.png b/code/05-using-an-action-creator-thunk/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/05-using-an-action-creator-thunk/public/logo512.png differ diff --git a/code/05-using-an-action-creator-thunk/public/manifest.json b/code/05-using-an-action-creator-thunk/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/05-using-an-action-creator-thunk/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/05-using-an-action-creator-thunk/public/robots.txt b/code/05-using-an-action-creator-thunk/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/05-using-an-action-creator-thunk/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/05-using-an-action-creator-thunk/src/App.js b/code/05-using-an-action-creator-thunk/src/App.js new file mode 100644 index 0000000000..2464d31f8a --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/App.js @@ -0,0 +1,44 @@ +import { Fragment, useEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; + +import Cart from './components/Cart/Cart'; +import Layout from './components/Layout/Layout'; +import Products from './components/Shop/Products'; +import Notification from './components/UI/Notification'; +import { sendCartData } from './store/cart-slice'; + +let isInitial = true; + +function App() { + const dispatch = useDispatch(); + const showCart = useSelector((state) => state.ui.cartIsVisible); + const cart = useSelector((state) => state.cart); + const notification = useSelector((state) => state.ui.notification); + + useEffect(() => { + if (isInitial) { + isInitial = false; + return; + } + + dispatch(sendCartData(cart)); + }, [cart, dispatch]); + + return ( + + {notification && ( + + )} + + {showCart && } + + + + ); +} + +export default App; diff --git a/code/05-using-an-action-creator-thunk/src/components/Cart/Cart.js b/code/05-using-an-action-creator-thunk/src/components/Cart/Cart.js new file mode 100644 index 0000000000..33030d2089 --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/components/Cart/Cart.js @@ -0,0 +1,31 @@ +import { useSelector } from 'react-redux'; + +import Card from '../UI/Card'; +import classes from './Cart.module.css'; +import CartItem from './CartItem'; + +const Cart = (props) => { + const cartItems = useSelector((state) => state.cart.items); + + return ( + +

    Your Shopping Cart

    + +
    + ); +}; + +export default Cart; diff --git a/code/05-using-an-action-creator-thunk/src/components/Cart/Cart.module.css b/code/05-using-an-action-creator-thunk/src/components/Cart/Cart.module.css new file mode 100644 index 0000000000..95670ab70b --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/components/Cart/Cart.module.css @@ -0,0 +1,16 @@ +.cart { + max-width: 30rem; + background-color: #313131; + color: white; +} + +.cart h2 { + font-size: 1.25rem; + margin: 0.5rem 0; +} + +.cart ul { + list-style: none; + padding: 0; + margin: 0; +} \ No newline at end of file diff --git a/code/05-using-an-action-creator-thunk/src/components/Cart/CartButton.js b/code/05-using-an-action-creator-thunk/src/components/Cart/CartButton.js new file mode 100644 index 0000000000..4e59baac3c --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/components/Cart/CartButton.js @@ -0,0 +1,22 @@ +import { useDispatch, useSelector } from 'react-redux'; + +import { uiActions } from '../../store/ui-slice'; +import classes from './CartButton.module.css'; + +const CartButton = (props) => { + const dispatch = useDispatch(); + const cartQuantity = useSelector((state) => state.cart.totalQuantity); + + const toggleCartHandler = () => { + dispatch(uiActions.toggle()); + }; + + return ( + + ); +}; + +export default CartButton; diff --git a/code/05-using-an-action-creator-thunk/src/components/Cart/CartButton.module.css b/code/05-using-an-action-creator-thunk/src/components/Cart/CartButton.module.css new file mode 100644 index 0000000000..93445f4596 --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/components/Cart/CartButton.module.css @@ -0,0 +1,21 @@ +.button { + background-color: transparent; + border-color: #1ad1b9; + color: #1ad1b9; +} + +.button:hover, +.button:active { + color: white; +} + +.button span { + margin: 0 0.5rem; +} + +.badge { + background-color: #1ad1b9; + border-radius: 30px; + padding: 0.15rem 1.25rem; + color: #1d1d1d; +} \ No newline at end of file diff --git a/code/05-using-an-action-creator-thunk/src/components/Cart/CartItem.js b/code/05-using-an-action-creator-thunk/src/components/Cart/CartItem.js new file mode 100644 index 0000000000..e1c6a8f012 --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/components/Cart/CartItem.js @@ -0,0 +1,47 @@ +import { useDispatch } from 'react-redux'; + +import classes from './CartItem.module.css'; +import { cartActions } from '../../store/cart-slice'; + +const CartItem = (props) => { + const dispatch = useDispatch(); + + const { title, quantity, total, price, id } = props.item; + + const removeItemHandler = () => { + dispatch(cartActions.removeItemFromCart(id)); + }; + + const addItemHandler = () => { + dispatch( + cartActions.addItemToCart({ + id, + title, + price, + }) + ); + }; + + return ( +
  • +
    +

    {title}

    +
    + ${total.toFixed(2)}{' '} + (${price.toFixed(2)}/item) +
    +
    +
    +
    + x {quantity} +
    +
    + + +
    +
    +
  • + ); +}; + +export default CartItem; diff --git a/code/05-using-an-action-creator-thunk/src/components/Cart/CartItem.module.css b/code/05-using-an-action-creator-thunk/src/components/Cart/CartItem.module.css new file mode 100644 index 0000000000..e34100d841 --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/components/Cart/CartItem.module.css @@ -0,0 +1,58 @@ +.item { + margin: 1rem 0; + background-color: #575757; + padding: 1rem; +} + +.item h3 { + margin: 0 0 0.5rem 0; + font-size: 1.75rem; +} + +.item header { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.details { + display: flex; + justify-content: space-between; + align-items: center; +} + +.quantity span { + font-size: 1.5rem; + font-weight: bold; +} + +.price { + font-size: 1.5rem; + font-weight: bold; +} + +.itemprice { + font-weight: normal; + font-size: 1rem; + font-style: italic; +} + +.actions { + display: flex; + justify-content: flex-end; + margin: 0.5rem 0; +} + +.actions button { + background-color: transparent; + border: 1px solid white; + margin-left: 0.5rem; + padding: 0.15rem 1rem; + color: white; +} + +.actions button:hover, +.actions button:active { + background-color: #4b4b4b; + color: white; +} \ No newline at end of file diff --git a/code/05-using-an-action-creator-thunk/src/components/Layout/Layout.js b/code/05-using-an-action-creator-thunk/src/components/Layout/Layout.js new file mode 100644 index 0000000000..0ce12a011f --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/components/Layout/Layout.js @@ -0,0 +1,13 @@ +import { Fragment } from 'react'; +import MainHeader from './MainHeader'; + +const Layout = (props) => { + return ( + + +
    {props.children}
    +
    + ); +}; + +export default Layout; diff --git a/code/05-using-an-action-creator-thunk/src/components/Layout/MainHeader.js b/code/05-using-an-action-creator-thunk/src/components/Layout/MainHeader.js new file mode 100644 index 0000000000..38ea37a29b --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/components/Layout/MainHeader.js @@ -0,0 +1,19 @@ +import CartButton from '../Cart/CartButton'; +import classes from './MainHeader.module.css'; + +const MainHeader = (props) => { + return ( +
    +

    ReduxCart

    + +
    + ); +}; + +export default MainHeader; diff --git a/code/05-using-an-action-creator-thunk/src/components/Layout/MainHeader.module.css b/code/05-using-an-action-creator-thunk/src/components/Layout/MainHeader.module.css new file mode 100644 index 0000000000..e41c98d247 --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/components/Layout/MainHeader.module.css @@ -0,0 +1,19 @@ +.header { + width: 100%; + height: 5rem; + padding: 0 10%; + display: flex; + align-items: center; + justify-content: space-between; + background-color: #252424; +} + +.header h1 { + color: white; +} + +.header ul { + list-style: none; + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/code/05-using-an-action-creator-thunk/src/components/Shop/ProductItem.js b/code/05-using-an-action-creator-thunk/src/components/Shop/ProductItem.js new file mode 100644 index 0000000000..4a2a4b3f9f --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/components/Shop/ProductItem.js @@ -0,0 +1,41 @@ +import { useDispatch } from 'react-redux'; + +import { cartActions } from '../../store/cart-slice'; +import Card from '../UI/Card'; +import classes from './ProductItem.module.css'; + +const ProductItem = (props) => { + const dispatch = useDispatch(); + + const { title, price, description, id } = props; + + const addToCartHandler = () => { + // and then send Http request + // fetch('firebase-url', { method: 'POST', body: JSON.stringify(newCart) }) + + dispatch( + cartActions.addItemToCart({ + id, + title, + price, + }) + ); + }; + + return ( +
  • + +
    +

    {title}

    +
    ${price.toFixed(2)}
    +
    +

    {description}

    +
    + +
    +
    +
  • + ); +}; + +export default ProductItem; diff --git a/code/05-using-an-action-creator-thunk/src/components/Shop/ProductItem.module.css b/code/05-using-an-action-creator-thunk/src/components/Shop/ProductItem.module.css new file mode 100644 index 0000000000..2fcdc5d8a6 --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/components/Shop/ProductItem.module.css @@ -0,0 +1,27 @@ +.item h3 { + margin: 0.5rem 0; + font-size: 1.25rem; +} + +.item header { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.price { + border-radius: 30px; + padding: 0.15rem 1.5rem; + background-color: #3a3a3a; + color: white; + font-size: 1.5rem; +} + +.item p { + color: #3a3a3a; +} + +.actions { + display: flex; + justify-content: flex-end; +} \ No newline at end of file diff --git a/code/05-using-an-action-creator-thunk/src/components/Shop/Products.js b/code/05-using-an-action-creator-thunk/src/components/Shop/Products.js new file mode 100644 index 0000000000..6b430cc262 --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/components/Shop/Products.js @@ -0,0 +1,38 @@ +import ProductItem from './ProductItem'; +import classes from './Products.module.css'; + +const DUMMY_PRODUCTS = [ + { + id: 'p1', + price: 6, + title: 'My First Book', + description: 'The first book I ever wrote', + }, + { + id: 'p2', + price: 5, + title: 'My Second Book', + description: 'The second book I ever wrote', + }, +]; + +const Products = (props) => { + return ( +
    +

    Buy your favorite products

    + +
    + ); +}; + +export default Products; diff --git a/code/05-using-an-action-creator-thunk/src/components/Shop/Products.module.css b/code/05-using-an-action-creator-thunk/src/components/Shop/Products.module.css new file mode 100644 index 0000000000..d81c97330f --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/components/Shop/Products.module.css @@ -0,0 +1,12 @@ +.products h2 { + color: white; + margin: 2rem auto; + text-align: center; + text-transform: uppercase; +} + +.products ul { + list-style: none; + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/code/05-using-an-action-creator-thunk/src/components/UI/Card.js b/code/05-using-an-action-creator-thunk/src/components/UI/Card.js new file mode 100644 index 0000000000..849202f21c --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/components/UI/Card.js @@ -0,0 +1,13 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return ( +
    + {props.children} +
    + ); +}; + +export default Card; diff --git a/code/05-using-an-action-creator-thunk/src/components/UI/Card.module.css b/code/05-using-an-action-creator-thunk/src/components/UI/Card.module.css new file mode 100644 index 0000000000..ac9c6709f4 --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/components/UI/Card.module.css @@ -0,0 +1,8 @@ +.card { + margin: 1rem auto; + border-radius: 6px; + background-color: white; + padding: 1rem; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/05-using-an-action-creator-thunk/src/components/UI/Notification.js b/code/05-using-an-action-creator-thunk/src/components/UI/Notification.js new file mode 100644 index 0000000000..982017fa74 --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/components/UI/Notification.js @@ -0,0 +1,23 @@ +import classes from './Notification.module.css'; + +const Notification = (props) => { + let specialClasses = ''; + + if (props.status === 'error') { + specialClasses = classes.error; + } + if (props.status === 'success') { + specialClasses = classes.success; + } + + const cssClasses = `${classes.notification} ${specialClasses}`; + + return ( +
    +

    {props.title}

    +

    {props.message}

    +
    + ); +}; + +export default Notification; diff --git a/code/05-using-an-action-creator-thunk/src/components/UI/Notification.module.css b/code/05-using-an-action-creator-thunk/src/components/UI/Notification.module.css new file mode 100644 index 0000000000..5decd98ea5 --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/components/UI/Notification.module.css @@ -0,0 +1,24 @@ +.notification { + width: 100%; + height: 3rem; + background-color: #1a8ed1; + display: flex; + justify-content: space-between; + padding: 0.5rem 10%; + align-items: center; + color: white; +} + +.notification h2, +.notification p { + font-size: 1rem; + margin: 0; +} + +.error { + background-color: #690000; +} + +.success { + background-color: #1ad1b9; +} \ No newline at end of file diff --git a/code/05-using-an-action-creator-thunk/src/index.css b/code/05-using-an-action-creator-thunk/src/index.css new file mode 100644 index 0000000000..3431f5f884 --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/index.css @@ -0,0 +1,31 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'); + +* { + box-sizing: border-box; +} + +html { + font-family: 'Noto Sans JP', sans-serif; +} + +body { + margin: 0; + background-color: #3f3f3f; +} + +button { + font: inherit; + cursor: pointer; + padding: 0.5rem 1.5rem; + border-radius: 6px; + background-color: transparent; + color: #1a8ed1; + border: 1px solid #1a8ed1; +} + +button:hover, +button:active { + background-color: #1ac5d1; + border-color: #1ac5d1; + color: white; +} \ No newline at end of file diff --git a/code/05-using-an-action-creator-thunk/src/index.js b/code/05-using-an-action-creator-thunk/src/index.js new file mode 100644 index 0000000000..c67c8acc68 --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/index.js @@ -0,0 +1,13 @@ +import ReactDOM from 'react-dom/client'; +import { Provider } from 'react-redux'; + +import store from './store/index'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/05-using-an-action-creator-thunk/src/store/cart-slice.js b/code/05-using-an-action-creator-thunk/src/store/cart-slice.js new file mode 100644 index 0000000000..c096bb67d7 --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/store/cart-slice.js @@ -0,0 +1,94 @@ +import { createSlice } from '@reduxjs/toolkit'; + +import { uiActions } from './ui-slice'; + +const cartSlice = createSlice({ + name: 'cart', + initialState: { + items: [], + totalQuantity: 0, + }, + reducers: { + replaceCart(state, action) { + state.totalQuantity = action.payload.totalQuantity; + state.items = action.payload.items; + }, + addItemToCart(state, action) { + const newItem = action.payload; + const existingItem = state.items.find((item) => item.id === newItem.id); + state.totalQuantity++; + if (!existingItem) { + state.items.push({ + id: newItem.id, + price: newItem.price, + quantity: 1, + totalPrice: newItem.price, + name: newItem.title, + }); + } else { + existingItem.quantity++; + existingItem.totalPrice = existingItem.totalPrice + newItem.price; + } + }, + removeItemFromCart(state, action) { + const id = action.payload; + const existingItem = state.items.find((item) => item.id === id); + state.totalQuantity--; + if (existingItem.quantity === 1) { + state.items = state.items.filter((item) => item.id !== id); + } else { + existingItem.quantity--; + } + }, + }, +}); + +export const sendCartData = (cart) => { + return async (dispatch) => { + dispatch( + uiActions.showNotification({ + status: 'pending', + title: 'Sending...', + message: 'Sending cart data!', + }) + ); + + const sendRequest = async () => { + const response = await fetch( + 'https://react-http-6b4a6.firebaseio.com/cart.json', + { + method: 'PUT', + body: JSON.stringify(cart), + } + ); + + if (!response.ok) { + throw new Error('Sending cart data failed.'); + } + }; + + try { + await sendRequest(); + + dispatch( + uiActions.showNotification({ + status: 'success', + title: 'Success!', + message: 'Sent cart data successfully!', + }) + ); + } catch (error) { + dispatch( + uiActions.showNotification({ + status: 'error', + title: 'Error!', + message: 'Sending cart data failed!', + }) + ); + } + }; +}; + +export const cartActions = cartSlice.actions; + +export default cartSlice; diff --git a/code/05-using-an-action-creator-thunk/src/store/index.js b/code/05-using-an-action-creator-thunk/src/store/index.js new file mode 100644 index 0000000000..d663f26de2 --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/store/index.js @@ -0,0 +1,10 @@ +import { configureStore } from '@reduxjs/toolkit'; + +import uiSlice from './ui-slice'; +import cartSlice from './cart-slice'; + +const store = configureStore({ + reducer: { ui: uiSlice.reducer, cart: cartSlice.reducer }, +}); + +export default store; diff --git a/code/05-using-an-action-creator-thunk/src/store/ui-slice.js b/code/05-using-an-action-creator-thunk/src/store/ui-slice.js new file mode 100644 index 0000000000..a25529a6f0 --- /dev/null +++ b/code/05-using-an-action-creator-thunk/src/store/ui-slice.js @@ -0,0 +1,22 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const uiSlice = createSlice({ + name: 'ui', + initialState: { cartIsVisible: false, notification: null }, + reducers: { + toggle(state) { + state.cartIsVisible = !state.cartIsVisible; + }, + showNotification(state, action) { + state.notification = { + status: action.payload.status, + title: action.payload.title, + message: action.payload.message, + }; + }, + }, +}); + +export const uiActions = uiSlice.actions; + +export default uiSlice; diff --git a/code/06-finished/package.json b/code/06-finished/package.json new file mode 100644 index 0000000000..99c15d93ce --- /dev/null +++ b/code/06-finished/package.json @@ -0,0 +1,40 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@reduxjs/toolkit": "^1.5.0", + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-redux": "^7.2.2", + "react-scripts": "4.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/06-finished/public/favicon.ico b/code/06-finished/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/06-finished/public/favicon.ico differ diff --git a/code/06-finished/public/index.html b/code/06-finished/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/06-finished/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/code/06-finished/public/logo192.png b/code/06-finished/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/06-finished/public/logo192.png differ diff --git a/code/06-finished/public/logo512.png b/code/06-finished/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/06-finished/public/logo512.png differ diff --git a/code/06-finished/public/manifest.json b/code/06-finished/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/06-finished/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/06-finished/public/robots.txt b/code/06-finished/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/06-finished/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/06-finished/src/App.js b/code/06-finished/src/App.js new file mode 100644 index 0000000000..b20071be84 --- /dev/null +++ b/code/06-finished/src/App.js @@ -0,0 +1,50 @@ +import { Fragment, useEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; + +import Cart from './components/Cart/Cart'; +import Layout from './components/Layout/Layout'; +import Products from './components/Shop/Products'; +import Notification from './components/UI/Notification'; +import { sendCartData, fetchCartData } from './store/cart-actions'; + +let isInitial = true; + +function App() { + const dispatch = useDispatch(); + const showCart = useSelector((state) => state.ui.cartIsVisible); + const cart = useSelector((state) => state.cart); + const notification = useSelector((state) => state.ui.notification); + + useEffect(() => { + dispatch(fetchCartData()); + }, [dispatch]); + + useEffect(() => { + if (isInitial) { + isInitial = false; + return; + } + + if (cart.changed) { + dispatch(sendCartData(cart)); + } + }, [cart, dispatch]); + + return ( + + {notification && ( + + )} + + {showCart && } + + + + ); +} + +export default App; diff --git a/code/06-finished/src/components/Cart/Cart.js b/code/06-finished/src/components/Cart/Cart.js new file mode 100644 index 0000000000..33030d2089 --- /dev/null +++ b/code/06-finished/src/components/Cart/Cart.js @@ -0,0 +1,31 @@ +import { useSelector } from 'react-redux'; + +import Card from '../UI/Card'; +import classes from './Cart.module.css'; +import CartItem from './CartItem'; + +const Cart = (props) => { + const cartItems = useSelector((state) => state.cart.items); + + return ( + +

    Your Shopping Cart

    + +
    + ); +}; + +export default Cart; diff --git a/code/06-finished/src/components/Cart/Cart.module.css b/code/06-finished/src/components/Cart/Cart.module.css new file mode 100644 index 0000000000..95670ab70b --- /dev/null +++ b/code/06-finished/src/components/Cart/Cart.module.css @@ -0,0 +1,16 @@ +.cart { + max-width: 30rem; + background-color: #313131; + color: white; +} + +.cart h2 { + font-size: 1.25rem; + margin: 0.5rem 0; +} + +.cart ul { + list-style: none; + padding: 0; + margin: 0; +} \ No newline at end of file diff --git a/code/06-finished/src/components/Cart/CartButton.js b/code/06-finished/src/components/Cart/CartButton.js new file mode 100644 index 0000000000..4e59baac3c --- /dev/null +++ b/code/06-finished/src/components/Cart/CartButton.js @@ -0,0 +1,22 @@ +import { useDispatch, useSelector } from 'react-redux'; + +import { uiActions } from '../../store/ui-slice'; +import classes from './CartButton.module.css'; + +const CartButton = (props) => { + const dispatch = useDispatch(); + const cartQuantity = useSelector((state) => state.cart.totalQuantity); + + const toggleCartHandler = () => { + dispatch(uiActions.toggle()); + }; + + return ( + + ); +}; + +export default CartButton; diff --git a/code/06-finished/src/components/Cart/CartButton.module.css b/code/06-finished/src/components/Cart/CartButton.module.css new file mode 100644 index 0000000000..93445f4596 --- /dev/null +++ b/code/06-finished/src/components/Cart/CartButton.module.css @@ -0,0 +1,21 @@ +.button { + background-color: transparent; + border-color: #1ad1b9; + color: #1ad1b9; +} + +.button:hover, +.button:active { + color: white; +} + +.button span { + margin: 0 0.5rem; +} + +.badge { + background-color: #1ad1b9; + border-radius: 30px; + padding: 0.15rem 1.25rem; + color: #1d1d1d; +} \ No newline at end of file diff --git a/code/06-finished/src/components/Cart/CartItem.js b/code/06-finished/src/components/Cart/CartItem.js new file mode 100644 index 0000000000..e1c6a8f012 --- /dev/null +++ b/code/06-finished/src/components/Cart/CartItem.js @@ -0,0 +1,47 @@ +import { useDispatch } from 'react-redux'; + +import classes from './CartItem.module.css'; +import { cartActions } from '../../store/cart-slice'; + +const CartItem = (props) => { + const dispatch = useDispatch(); + + const { title, quantity, total, price, id } = props.item; + + const removeItemHandler = () => { + dispatch(cartActions.removeItemFromCart(id)); + }; + + const addItemHandler = () => { + dispatch( + cartActions.addItemToCart({ + id, + title, + price, + }) + ); + }; + + return ( +
  • +
    +

    {title}

    +
    + ${total.toFixed(2)}{' '} + (${price.toFixed(2)}/item) +
    +
    +
    +
    + x {quantity} +
    +
    + + +
    +
    +
  • + ); +}; + +export default CartItem; diff --git a/code/06-finished/src/components/Cart/CartItem.module.css b/code/06-finished/src/components/Cart/CartItem.module.css new file mode 100644 index 0000000000..e34100d841 --- /dev/null +++ b/code/06-finished/src/components/Cart/CartItem.module.css @@ -0,0 +1,58 @@ +.item { + margin: 1rem 0; + background-color: #575757; + padding: 1rem; +} + +.item h3 { + margin: 0 0 0.5rem 0; + font-size: 1.75rem; +} + +.item header { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.details { + display: flex; + justify-content: space-between; + align-items: center; +} + +.quantity span { + font-size: 1.5rem; + font-weight: bold; +} + +.price { + font-size: 1.5rem; + font-weight: bold; +} + +.itemprice { + font-weight: normal; + font-size: 1rem; + font-style: italic; +} + +.actions { + display: flex; + justify-content: flex-end; + margin: 0.5rem 0; +} + +.actions button { + background-color: transparent; + border: 1px solid white; + margin-left: 0.5rem; + padding: 0.15rem 1rem; + color: white; +} + +.actions button:hover, +.actions button:active { + background-color: #4b4b4b; + color: white; +} \ No newline at end of file diff --git a/code/06-finished/src/components/Layout/Layout.js b/code/06-finished/src/components/Layout/Layout.js new file mode 100644 index 0000000000..0ce12a011f --- /dev/null +++ b/code/06-finished/src/components/Layout/Layout.js @@ -0,0 +1,13 @@ +import { Fragment } from 'react'; +import MainHeader from './MainHeader'; + +const Layout = (props) => { + return ( + + +
    {props.children}
    +
    + ); +}; + +export default Layout; diff --git a/code/06-finished/src/components/Layout/MainHeader.js b/code/06-finished/src/components/Layout/MainHeader.js new file mode 100644 index 0000000000..38ea37a29b --- /dev/null +++ b/code/06-finished/src/components/Layout/MainHeader.js @@ -0,0 +1,19 @@ +import CartButton from '../Cart/CartButton'; +import classes from './MainHeader.module.css'; + +const MainHeader = (props) => { + return ( +
    +

    ReduxCart

    + +
    + ); +}; + +export default MainHeader; diff --git a/code/06-finished/src/components/Layout/MainHeader.module.css b/code/06-finished/src/components/Layout/MainHeader.module.css new file mode 100644 index 0000000000..e41c98d247 --- /dev/null +++ b/code/06-finished/src/components/Layout/MainHeader.module.css @@ -0,0 +1,19 @@ +.header { + width: 100%; + height: 5rem; + padding: 0 10%; + display: flex; + align-items: center; + justify-content: space-between; + background-color: #252424; +} + +.header h1 { + color: white; +} + +.header ul { + list-style: none; + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/code/06-finished/src/components/Shop/ProductItem.js b/code/06-finished/src/components/Shop/ProductItem.js new file mode 100644 index 0000000000..4a2a4b3f9f --- /dev/null +++ b/code/06-finished/src/components/Shop/ProductItem.js @@ -0,0 +1,41 @@ +import { useDispatch } from 'react-redux'; + +import { cartActions } from '../../store/cart-slice'; +import Card from '../UI/Card'; +import classes from './ProductItem.module.css'; + +const ProductItem = (props) => { + const dispatch = useDispatch(); + + const { title, price, description, id } = props; + + const addToCartHandler = () => { + // and then send Http request + // fetch('firebase-url', { method: 'POST', body: JSON.stringify(newCart) }) + + dispatch( + cartActions.addItemToCart({ + id, + title, + price, + }) + ); + }; + + return ( +
  • + +
    +

    {title}

    +
    ${price.toFixed(2)}
    +
    +

    {description}

    +
    + +
    +
    +
  • + ); +}; + +export default ProductItem; diff --git a/code/06-finished/src/components/Shop/ProductItem.module.css b/code/06-finished/src/components/Shop/ProductItem.module.css new file mode 100644 index 0000000000..2fcdc5d8a6 --- /dev/null +++ b/code/06-finished/src/components/Shop/ProductItem.module.css @@ -0,0 +1,27 @@ +.item h3 { + margin: 0.5rem 0; + font-size: 1.25rem; +} + +.item header { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.price { + border-radius: 30px; + padding: 0.15rem 1.5rem; + background-color: #3a3a3a; + color: white; + font-size: 1.5rem; +} + +.item p { + color: #3a3a3a; +} + +.actions { + display: flex; + justify-content: flex-end; +} \ No newline at end of file diff --git a/code/06-finished/src/components/Shop/Products.js b/code/06-finished/src/components/Shop/Products.js new file mode 100644 index 0000000000..6b430cc262 --- /dev/null +++ b/code/06-finished/src/components/Shop/Products.js @@ -0,0 +1,38 @@ +import ProductItem from './ProductItem'; +import classes from './Products.module.css'; + +const DUMMY_PRODUCTS = [ + { + id: 'p1', + price: 6, + title: 'My First Book', + description: 'The first book I ever wrote', + }, + { + id: 'p2', + price: 5, + title: 'My Second Book', + description: 'The second book I ever wrote', + }, +]; + +const Products = (props) => { + return ( +
    +

    Buy your favorite products

    + +
    + ); +}; + +export default Products; diff --git a/code/06-finished/src/components/Shop/Products.module.css b/code/06-finished/src/components/Shop/Products.module.css new file mode 100644 index 0000000000..d81c97330f --- /dev/null +++ b/code/06-finished/src/components/Shop/Products.module.css @@ -0,0 +1,12 @@ +.products h2 { + color: white; + margin: 2rem auto; + text-align: center; + text-transform: uppercase; +} + +.products ul { + list-style: none; + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/code/06-finished/src/components/UI/Card.js b/code/06-finished/src/components/UI/Card.js new file mode 100644 index 0000000000..849202f21c --- /dev/null +++ b/code/06-finished/src/components/UI/Card.js @@ -0,0 +1,13 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return ( +
    + {props.children} +
    + ); +}; + +export default Card; diff --git a/code/06-finished/src/components/UI/Card.module.css b/code/06-finished/src/components/UI/Card.module.css new file mode 100644 index 0000000000..ac9c6709f4 --- /dev/null +++ b/code/06-finished/src/components/UI/Card.module.css @@ -0,0 +1,8 @@ +.card { + margin: 1rem auto; + border-radius: 6px; + background-color: white; + padding: 1rem; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/06-finished/src/components/UI/Notification.js b/code/06-finished/src/components/UI/Notification.js new file mode 100644 index 0000000000..982017fa74 --- /dev/null +++ b/code/06-finished/src/components/UI/Notification.js @@ -0,0 +1,23 @@ +import classes from './Notification.module.css'; + +const Notification = (props) => { + let specialClasses = ''; + + if (props.status === 'error') { + specialClasses = classes.error; + } + if (props.status === 'success') { + specialClasses = classes.success; + } + + const cssClasses = `${classes.notification} ${specialClasses}`; + + return ( +
    +

    {props.title}

    +

    {props.message}

    +
    + ); +}; + +export default Notification; diff --git a/code/06-finished/src/components/UI/Notification.module.css b/code/06-finished/src/components/UI/Notification.module.css new file mode 100644 index 0000000000..5decd98ea5 --- /dev/null +++ b/code/06-finished/src/components/UI/Notification.module.css @@ -0,0 +1,24 @@ +.notification { + width: 100%; + height: 3rem; + background-color: #1a8ed1; + display: flex; + justify-content: space-between; + padding: 0.5rem 10%; + align-items: center; + color: white; +} + +.notification h2, +.notification p { + font-size: 1rem; + margin: 0; +} + +.error { + background-color: #690000; +} + +.success { + background-color: #1ad1b9; +} \ No newline at end of file diff --git a/code/06-finished/src/index.css b/code/06-finished/src/index.css new file mode 100644 index 0000000000..3431f5f884 --- /dev/null +++ b/code/06-finished/src/index.css @@ -0,0 +1,31 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'); + +* { + box-sizing: border-box; +} + +html { + font-family: 'Noto Sans JP', sans-serif; +} + +body { + margin: 0; + background-color: #3f3f3f; +} + +button { + font: inherit; + cursor: pointer; + padding: 0.5rem 1.5rem; + border-radius: 6px; + background-color: transparent; + color: #1a8ed1; + border: 1px solid #1a8ed1; +} + +button:hover, +button:active { + background-color: #1ac5d1; + border-color: #1ac5d1; + color: white; +} \ No newline at end of file diff --git a/code/06-finished/src/index.js b/code/06-finished/src/index.js new file mode 100644 index 0000000000..c67c8acc68 --- /dev/null +++ b/code/06-finished/src/index.js @@ -0,0 +1,13 @@ +import ReactDOM from 'react-dom/client'; +import { Provider } from 'react-redux'; + +import store from './store/index'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/06-finished/src/store/cart-actions.js b/code/06-finished/src/store/cart-actions.js new file mode 100644 index 0000000000..4459999b4f --- /dev/null +++ b/code/06-finished/src/store/cart-actions.js @@ -0,0 +1,87 @@ +import { uiActions } from './ui-slice'; +import { cartActions } from './cart-slice'; + +export const fetchCartData = () => { + return async (dispatch) => { + const fetchData = async () => { + const response = await fetch( + 'https://react-http-6b4a6.firebaseio.com/cart.json' + ); + + if (!response.ok) { + throw new Error('Could not fetch cart data!'); + } + + const data = await response.json(); + + return data; + }; + + try { + const cartData = await fetchData(); + dispatch( + cartActions.replaceCart({ + items: cartData.items || [], + totalQuantity: cartData.totalQuantity, + }) + ); + } catch (error) { + dispatch( + uiActions.showNotification({ + status: 'error', + title: 'Error!', + message: 'Fetching cart data failed!', + }) + ); + } + }; +}; + +export const sendCartData = (cart) => { + return async (dispatch) => { + dispatch( + uiActions.showNotification({ + status: 'pending', + title: 'Sending...', + message: 'Sending cart data!', + }) + ); + + const sendRequest = async () => { + const response = await fetch( + 'https://react-http-6b4a6.firebaseio.com/cart.json', + { + method: 'PUT', + body: JSON.stringify({ + items: cart.items, + totalQuantity: cart.totalQuantity, + }), + } + ); + + if (!response.ok) { + throw new Error('Sending cart data failed.'); + } + }; + + try { + await sendRequest(); + + dispatch( + uiActions.showNotification({ + status: 'success', + title: 'Success!', + message: 'Sent cart data successfully!', + }) + ); + } catch (error) { + dispatch( + uiActions.showNotification({ + status: 'error', + title: 'Error!', + message: 'Sending cart data failed!', + }) + ); + } + }; +}; diff --git a/code/06-finished/src/store/cart-slice.js b/code/06-finished/src/store/cart-slice.js new file mode 100644 index 0000000000..c4a7abc07e --- /dev/null +++ b/code/06-finished/src/store/cart-slice.js @@ -0,0 +1,50 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const cartSlice = createSlice({ + name: 'cart', + initialState: { + items: [], + totalQuantity: 0, + changed: false, + }, + reducers: { + replaceCart(state, action) { + state.totalQuantity = action.payload.totalQuantity; + state.items = action.payload.items; + }, + addItemToCart(state, action) { + const newItem = action.payload; + const existingItem = state.items.find((item) => item.id === newItem.id); + state.totalQuantity++; + state.changed = true; + if (!existingItem) { + state.items.push({ + id: newItem.id, + price: newItem.price, + quantity: 1, + totalPrice: newItem.price, + name: newItem.title, + }); + } else { + existingItem.quantity++; + existingItem.totalPrice = existingItem.totalPrice + newItem.price; + } + }, + removeItemFromCart(state, action) { + const id = action.payload; + const existingItem = state.items.find((item) => item.id === id); + state.totalQuantity--; + state.changed = true; + if (existingItem.quantity === 1) { + state.items = state.items.filter((item) => item.id !== id); + } else { + existingItem.quantity--; + existingItem.totalPrice = existingItem.totalPrice - existingItem.price; + } + }, + }, +}); + +export const cartActions = cartSlice.actions; + +export default cartSlice; diff --git a/code/06-finished/src/store/index.js b/code/06-finished/src/store/index.js new file mode 100644 index 0000000000..d663f26de2 --- /dev/null +++ b/code/06-finished/src/store/index.js @@ -0,0 +1,10 @@ +import { configureStore } from '@reduxjs/toolkit'; + +import uiSlice from './ui-slice'; +import cartSlice from './cart-slice'; + +const store = configureStore({ + reducer: { ui: uiSlice.reducer, cart: cartSlice.reducer }, +}); + +export default store; diff --git a/code/06-finished/src/store/ui-slice.js b/code/06-finished/src/store/ui-slice.js new file mode 100644 index 0000000000..a25529a6f0 --- /dev/null +++ b/code/06-finished/src/store/ui-slice.js @@ -0,0 +1,22 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const uiSlice = createSlice({ + name: 'ui', + initialState: { cartIsVisible: false, notification: null }, + reducers: { + toggle(state) { + state.cartIsVisible = !state.cartIsVisible; + }, + showNotification(state, action) { + state.notification = { + status: action.payload.status, + title: action.payload.title, + message: action.payload.message, + }; + }, + }, +}); + +export const uiActions = uiSlice.actions; + +export default uiSlice; diff --git a/code/zz-suboptimal-example-code/package.json b/code/zz-suboptimal-example-code/package.json new file mode 100644 index 0000000000..99c15d93ce --- /dev/null +++ b/code/zz-suboptimal-example-code/package.json @@ -0,0 +1,40 @@ +{ + "name": "react-complete-guide", + "version": "0.1.0", + "private": true, + "dependencies": { + "@reduxjs/toolkit": "^1.5.0", + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.5.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-redux": "^7.2.2", + "react-scripts": "4.0.1", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/code/zz-suboptimal-example-code/public/favicon.ico b/code/zz-suboptimal-example-code/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/code/zz-suboptimal-example-code/public/favicon.ico differ diff --git a/code/zz-suboptimal-example-code/public/index.html b/code/zz-suboptimal-example-code/public/index.html new file mode 100644 index 0000000000..aa069f27cb --- /dev/null +++ b/code/zz-suboptimal-example-code/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/code/zz-suboptimal-example-code/public/logo192.png b/code/zz-suboptimal-example-code/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/code/zz-suboptimal-example-code/public/logo192.png differ diff --git a/code/zz-suboptimal-example-code/public/logo512.png b/code/zz-suboptimal-example-code/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/code/zz-suboptimal-example-code/public/logo512.png differ diff --git a/code/zz-suboptimal-example-code/public/manifest.json b/code/zz-suboptimal-example-code/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/code/zz-suboptimal-example-code/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/code/zz-suboptimal-example-code/public/robots.txt b/code/zz-suboptimal-example-code/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/code/zz-suboptimal-example-code/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/code/zz-suboptimal-example-code/src/App.js b/code/zz-suboptimal-example-code/src/App.js new file mode 100644 index 0000000000..8531b2d78c --- /dev/null +++ b/code/zz-suboptimal-example-code/src/App.js @@ -0,0 +1,18 @@ +import { useSelector } from 'react-redux'; + +import Cart from './components/Cart/Cart'; +import Layout from './components/Layout/Layout'; +import Products from './components/Shop/Products'; + +function App() { + const showCart = useSelector((state) => state.ui.cartIsVisible); + + return ( + + {showCart && } + + + ); +} + +export default App; diff --git a/code/zz-suboptimal-example-code/src/components/Cart/Cart.js b/code/zz-suboptimal-example-code/src/components/Cart/Cart.js new file mode 100644 index 0000000000..33030d2089 --- /dev/null +++ b/code/zz-suboptimal-example-code/src/components/Cart/Cart.js @@ -0,0 +1,31 @@ +import { useSelector } from 'react-redux'; + +import Card from '../UI/Card'; +import classes from './Cart.module.css'; +import CartItem from './CartItem'; + +const Cart = (props) => { + const cartItems = useSelector((state) => state.cart.items); + + return ( + +

    Your Shopping Cart

    + +
    + ); +}; + +export default Cart; diff --git a/code/zz-suboptimal-example-code/src/components/Cart/Cart.module.css b/code/zz-suboptimal-example-code/src/components/Cart/Cart.module.css new file mode 100644 index 0000000000..95670ab70b --- /dev/null +++ b/code/zz-suboptimal-example-code/src/components/Cart/Cart.module.css @@ -0,0 +1,16 @@ +.cart { + max-width: 30rem; + background-color: #313131; + color: white; +} + +.cart h2 { + font-size: 1.25rem; + margin: 0.5rem 0; +} + +.cart ul { + list-style: none; + padding: 0; + margin: 0; +} \ No newline at end of file diff --git a/code/zz-suboptimal-example-code/src/components/Cart/CartButton.js b/code/zz-suboptimal-example-code/src/components/Cart/CartButton.js new file mode 100644 index 0000000000..4e59baac3c --- /dev/null +++ b/code/zz-suboptimal-example-code/src/components/Cart/CartButton.js @@ -0,0 +1,22 @@ +import { useDispatch, useSelector } from 'react-redux'; + +import { uiActions } from '../../store/ui-slice'; +import classes from './CartButton.module.css'; + +const CartButton = (props) => { + const dispatch = useDispatch(); + const cartQuantity = useSelector((state) => state.cart.totalQuantity); + + const toggleCartHandler = () => { + dispatch(uiActions.toggle()); + }; + + return ( + + ); +}; + +export default CartButton; diff --git a/code/zz-suboptimal-example-code/src/components/Cart/CartButton.module.css b/code/zz-suboptimal-example-code/src/components/Cart/CartButton.module.css new file mode 100644 index 0000000000..93445f4596 --- /dev/null +++ b/code/zz-suboptimal-example-code/src/components/Cart/CartButton.module.css @@ -0,0 +1,21 @@ +.button { + background-color: transparent; + border-color: #1ad1b9; + color: #1ad1b9; +} + +.button:hover, +.button:active { + color: white; +} + +.button span { + margin: 0 0.5rem; +} + +.badge { + background-color: #1ad1b9; + border-radius: 30px; + padding: 0.15rem 1.25rem; + color: #1d1d1d; +} \ No newline at end of file diff --git a/code/zz-suboptimal-example-code/src/components/Cart/CartItem.js b/code/zz-suboptimal-example-code/src/components/Cart/CartItem.js new file mode 100644 index 0000000000..e1c6a8f012 --- /dev/null +++ b/code/zz-suboptimal-example-code/src/components/Cart/CartItem.js @@ -0,0 +1,47 @@ +import { useDispatch } from 'react-redux'; + +import classes from './CartItem.module.css'; +import { cartActions } from '../../store/cart-slice'; + +const CartItem = (props) => { + const dispatch = useDispatch(); + + const { title, quantity, total, price, id } = props.item; + + const removeItemHandler = () => { + dispatch(cartActions.removeItemFromCart(id)); + }; + + const addItemHandler = () => { + dispatch( + cartActions.addItemToCart({ + id, + title, + price, + }) + ); + }; + + return ( +
  • +
    +

    {title}

    +
    + ${total.toFixed(2)}{' '} + (${price.toFixed(2)}/item) +
    +
    +
    +
    + x {quantity} +
    +
    + + +
    +
    +
  • + ); +}; + +export default CartItem; diff --git a/code/zz-suboptimal-example-code/src/components/Cart/CartItem.module.css b/code/zz-suboptimal-example-code/src/components/Cart/CartItem.module.css new file mode 100644 index 0000000000..e34100d841 --- /dev/null +++ b/code/zz-suboptimal-example-code/src/components/Cart/CartItem.module.css @@ -0,0 +1,58 @@ +.item { + margin: 1rem 0; + background-color: #575757; + padding: 1rem; +} + +.item h3 { + margin: 0 0 0.5rem 0; + font-size: 1.75rem; +} + +.item header { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.details { + display: flex; + justify-content: space-between; + align-items: center; +} + +.quantity span { + font-size: 1.5rem; + font-weight: bold; +} + +.price { + font-size: 1.5rem; + font-weight: bold; +} + +.itemprice { + font-weight: normal; + font-size: 1rem; + font-style: italic; +} + +.actions { + display: flex; + justify-content: flex-end; + margin: 0.5rem 0; +} + +.actions button { + background-color: transparent; + border: 1px solid white; + margin-left: 0.5rem; + padding: 0.15rem 1rem; + color: white; +} + +.actions button:hover, +.actions button:active { + background-color: #4b4b4b; + color: white; +} \ No newline at end of file diff --git a/code/zz-suboptimal-example-code/src/components/Layout/Layout.js b/code/zz-suboptimal-example-code/src/components/Layout/Layout.js new file mode 100644 index 0000000000..0ce12a011f --- /dev/null +++ b/code/zz-suboptimal-example-code/src/components/Layout/Layout.js @@ -0,0 +1,13 @@ +import { Fragment } from 'react'; +import MainHeader from './MainHeader'; + +const Layout = (props) => { + return ( + + +
    {props.children}
    +
    + ); +}; + +export default Layout; diff --git a/code/zz-suboptimal-example-code/src/components/Layout/MainHeader.js b/code/zz-suboptimal-example-code/src/components/Layout/MainHeader.js new file mode 100644 index 0000000000..38ea37a29b --- /dev/null +++ b/code/zz-suboptimal-example-code/src/components/Layout/MainHeader.js @@ -0,0 +1,19 @@ +import CartButton from '../Cart/CartButton'; +import classes from './MainHeader.module.css'; + +const MainHeader = (props) => { + return ( +
    +

    ReduxCart

    + +
    + ); +}; + +export default MainHeader; diff --git a/code/zz-suboptimal-example-code/src/components/Layout/MainHeader.module.css b/code/zz-suboptimal-example-code/src/components/Layout/MainHeader.module.css new file mode 100644 index 0000000000..e41c98d247 --- /dev/null +++ b/code/zz-suboptimal-example-code/src/components/Layout/MainHeader.module.css @@ -0,0 +1,19 @@ +.header { + width: 100%; + height: 5rem; + padding: 0 10%; + display: flex; + align-items: center; + justify-content: space-between; + background-color: #252424; +} + +.header h1 { + color: white; +} + +.header ul { + list-style: none; + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/code/zz-suboptimal-example-code/src/components/Shop/ProductItem.js b/code/zz-suboptimal-example-code/src/components/Shop/ProductItem.js new file mode 100644 index 0000000000..2c593ec625 --- /dev/null +++ b/code/zz-suboptimal-example-code/src/components/Shop/ProductItem.js @@ -0,0 +1,71 @@ +import { useDispatch, useSelector } from 'react-redux'; + +import { cartActions } from '../../store/cart-slice'; +import Card from '../UI/Card'; +import classes from './ProductItem.module.css'; + +const ProductItem = (props) => { + const cart = useSelector((state) => state.cart); + const dispatch = useDispatch(); + + const { title, price, description, id } = props; + + const addToCartHandler = () => { + const newTotalQuantity = cart.totalQuantity + 1; + + const updatedItems = cart.items.slice(); // create copy via slice to avoid mutating original state + const existingItem = updatedItems.find((item) => item.id === id); + if (existingItem) { + const updatedItem = { ...existingItem }; // new object + copy existing properties to avoid state mutation + updatedItem.quantity++; + updatedItem.totalPrice = updatedItem.totalPrice + price; + const existingItemIndex = updatedItems.findIndex( + (item) => item.id === id + ); + updatedItems[existingItemIndex] = updatedItem; + } else { + updatedItems.push({ + id: id, + price: price, + quantity: 1, + totalPrice: price, + name: title, + }); + } + + const newCart = { + totalQuantity: newTotalQuantity, + items: updatedItems, + }; + + dispatch(cartActions.replaceCart(newCart)); + + // and then send Http request + // fetch('firebase-url', { method: 'POST', body: JSON.stringify(newCart) }) + + // dispatch( + // cartActions.addItemToCart({ + // id, + // title, + // price, + // }) + // ); + }; + + return ( +
  • + +
    +

    {title}

    +
    ${price.toFixed(2)}
    +
    +

    {description}

    +
    + +
    +
    +
  • + ); +}; + +export default ProductItem; diff --git a/code/zz-suboptimal-example-code/src/components/Shop/ProductItem.module.css b/code/zz-suboptimal-example-code/src/components/Shop/ProductItem.module.css new file mode 100644 index 0000000000..2fcdc5d8a6 --- /dev/null +++ b/code/zz-suboptimal-example-code/src/components/Shop/ProductItem.module.css @@ -0,0 +1,27 @@ +.item h3 { + margin: 0.5rem 0; + font-size: 1.25rem; +} + +.item header { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.price { + border-radius: 30px; + padding: 0.15rem 1.5rem; + background-color: #3a3a3a; + color: white; + font-size: 1.5rem; +} + +.item p { + color: #3a3a3a; +} + +.actions { + display: flex; + justify-content: flex-end; +} \ No newline at end of file diff --git a/code/zz-suboptimal-example-code/src/components/Shop/Products.js b/code/zz-suboptimal-example-code/src/components/Shop/Products.js new file mode 100644 index 0000000000..6b430cc262 --- /dev/null +++ b/code/zz-suboptimal-example-code/src/components/Shop/Products.js @@ -0,0 +1,38 @@ +import ProductItem from './ProductItem'; +import classes from './Products.module.css'; + +const DUMMY_PRODUCTS = [ + { + id: 'p1', + price: 6, + title: 'My First Book', + description: 'The first book I ever wrote', + }, + { + id: 'p2', + price: 5, + title: 'My Second Book', + description: 'The second book I ever wrote', + }, +]; + +const Products = (props) => { + return ( +
    +

    Buy your favorite products

    + +
    + ); +}; + +export default Products; diff --git a/code/zz-suboptimal-example-code/src/components/Shop/Products.module.css b/code/zz-suboptimal-example-code/src/components/Shop/Products.module.css new file mode 100644 index 0000000000..d81c97330f --- /dev/null +++ b/code/zz-suboptimal-example-code/src/components/Shop/Products.module.css @@ -0,0 +1,12 @@ +.products h2 { + color: white; + margin: 2rem auto; + text-align: center; + text-transform: uppercase; +} + +.products ul { + list-style: none; + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/code/zz-suboptimal-example-code/src/components/UI/Card.js b/code/zz-suboptimal-example-code/src/components/UI/Card.js new file mode 100644 index 0000000000..849202f21c --- /dev/null +++ b/code/zz-suboptimal-example-code/src/components/UI/Card.js @@ -0,0 +1,13 @@ +import classes from './Card.module.css'; + +const Card = (props) => { + return ( +
    + {props.children} +
    + ); +}; + +export default Card; diff --git a/code/zz-suboptimal-example-code/src/components/UI/Card.module.css b/code/zz-suboptimal-example-code/src/components/UI/Card.module.css new file mode 100644 index 0000000000..ac9c6709f4 --- /dev/null +++ b/code/zz-suboptimal-example-code/src/components/UI/Card.module.css @@ -0,0 +1,8 @@ +.card { + margin: 1rem auto; + border-radius: 6px; + background-color: white; + padding: 1rem; + width: 90%; + max-width: 40rem; +} \ No newline at end of file diff --git a/code/zz-suboptimal-example-code/src/index.css b/code/zz-suboptimal-example-code/src/index.css new file mode 100644 index 0000000000..3431f5f884 --- /dev/null +++ b/code/zz-suboptimal-example-code/src/index.css @@ -0,0 +1,31 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap'); + +* { + box-sizing: border-box; +} + +html { + font-family: 'Noto Sans JP', sans-serif; +} + +body { + margin: 0; + background-color: #3f3f3f; +} + +button { + font: inherit; + cursor: pointer; + padding: 0.5rem 1.5rem; + border-radius: 6px; + background-color: transparent; + color: #1a8ed1; + border: 1px solid #1a8ed1; +} + +button:hover, +button:active { + background-color: #1ac5d1; + border-color: #1ac5d1; + color: white; +} \ No newline at end of file diff --git a/code/zz-suboptimal-example-code/src/index.js b/code/zz-suboptimal-example-code/src/index.js new file mode 100644 index 0000000000..c67c8acc68 --- /dev/null +++ b/code/zz-suboptimal-example-code/src/index.js @@ -0,0 +1,13 @@ +import ReactDOM from 'react-dom/client'; +import { Provider } from 'react-redux'; + +import store from './store/index'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/code/zz-suboptimal-example-code/src/store/cart-slice.js b/code/zz-suboptimal-example-code/src/store/cart-slice.js new file mode 100644 index 0000000000..e629fc66ce --- /dev/null +++ b/code/zz-suboptimal-example-code/src/store/cart-slice.js @@ -0,0 +1,46 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const cartSlice = createSlice({ + name: 'cart', + initialState: { + items: [], + totalQuantity: 0, + }, + reducers: { + replaceCart(state, action) { + state.totalQuantity = action.payload.totalQuantity; + state.items = action.payload.items; + }, + addItemToCart(state, action) { + const newItem = action.payload; + const existingItem = state.items.find((item) => item.id === newItem.id); + state.totalQuantity++; + if (!existingItem) { + state.items.push({ + id: newItem.id, + price: newItem.price, + quantity: 1, + totalPrice: newItem.price, + name: newItem.title, + }); + } else { + existingItem.quantity++; + existingItem.totalPrice = existingItem.totalPrice + newItem.price; + } + }, + removeItemFromCart(state, action) { + const id = action.payload; + const existingItem = state.items.find((item) => item.id === id); + state.totalQuantity--; + if (existingItem.quantity === 1) { + state.items = state.items.filter((item) => item.id !== id); + } else { + existingItem.quantity--; + } + }, + }, +}); + +export const cartActions = cartSlice.actions; + +export default cartSlice; diff --git a/code/zz-suboptimal-example-code/src/store/index.js b/code/zz-suboptimal-example-code/src/store/index.js new file mode 100644 index 0000000000..d663f26de2 --- /dev/null +++ b/code/zz-suboptimal-example-code/src/store/index.js @@ -0,0 +1,10 @@ +import { configureStore } from '@reduxjs/toolkit'; + +import uiSlice from './ui-slice'; +import cartSlice from './cart-slice'; + +const store = configureStore({ + reducer: { ui: uiSlice.reducer, cart: cartSlice.reducer }, +}); + +export default store; diff --git a/code/zz-suboptimal-example-code/src/store/ui-slice.js b/code/zz-suboptimal-example-code/src/store/ui-slice.js new file mode 100644 index 0000000000..4fa43c1b12 --- /dev/null +++ b/code/zz-suboptimal-example-code/src/store/ui-slice.js @@ -0,0 +1,15 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const uiSlice = createSlice({ + name: 'ui', + initialState: { cartIsVisible: false }, + reducers: { + toggle(state) { + state.cartIsVisible = !state.cartIsVisible; + } + } +}); + +export const uiActions = uiSlice.actions; + +export default uiSlice; \ No newline at end of file diff --git a/extra-files/Notification.js b/extra-files/Notification.js new file mode 100644 index 0000000000..982017fa74 --- /dev/null +++ b/extra-files/Notification.js @@ -0,0 +1,23 @@ +import classes from './Notification.module.css'; + +const Notification = (props) => { + let specialClasses = ''; + + if (props.status === 'error') { + specialClasses = classes.error; + } + if (props.status === 'success') { + specialClasses = classes.success; + } + + const cssClasses = `${classes.notification} ${specialClasses}`; + + return ( +
    +

    {props.title}

    +

    {props.message}

    +
    + ); +}; + +export default Notification; diff --git a/extra-files/Notification.module.css b/extra-files/Notification.module.css new file mode 100644 index 0000000000..5decd98ea5 --- /dev/null +++ b/extra-files/Notification.module.css @@ -0,0 +1,24 @@ +.notification { + width: 100%; + height: 3rem; + background-color: #1a8ed1; + display: flex; + justify-content: space-between; + padding: 0.5rem 10%; + align-items: center; + color: white; +} + +.notification h2, +.notification p { + font-size: 1rem; + margin: 0; +} + +.error { + background-color: #690000; +} + +.success { + background-color: #1ad1b9; +} \ No newline at end of file diff --git a/slides/slides.pdf b/slides/slides.pdf new file mode 100644 index 0000000000..9471d04995 Binary files /dev/null and b/slides/slides.pdf differ