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 (
+
+
+
+
+ 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 (
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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
+
+ {cartItems.map((item) => (
+
+ ))}
+
+
+ );
+};
+
+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 (
+
+
+
+
+ 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 (
+
+ );
+};
+
+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
+
+ {DUMMY_PRODUCTS.map((product) => (
+
+ ))}
+
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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
+
+ {cartItems.map((item) => (
+
+ ))}
+
+
+ );
+};
+
+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 (
+
+
+
+
+ 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 (
+
+ );
+};
+
+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
+
+ {DUMMY_PRODUCTS.map((product) => (
+
+ ))}
+
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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
+
+ {cartItems.map((item) => (
+
+ ))}
+
+
+ );
+};
+
+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 (
+
+
+
+
+ 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 (
+
+ );
+};
+
+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
+
+ {DUMMY_PRODUCTS.map((product) => (
+
+ ))}
+
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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
+
+ {cartItems.map((item) => (
+
+ ))}
+
+
+ );
+};
+
+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 (
+
+
+
+
+ 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 (
+
+ );
+};
+
+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
+
+ {DUMMY_PRODUCTS.map((product) => (
+
+ ))}
+
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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
+
+ {cartItems.map((item) => (
+
+ ))}
+
+
+ );
+};
+
+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 (
+
+
+
+
+ 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 (
+
+ );
+};
+
+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
+
+ {DUMMY_PRODUCTS.map((product) => (
+
+ ))}
+
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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
+
+ {cartItems.map((item) => (
+
+ ))}
+
+
+ );
+};
+
+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 (
+
+
+
+
+ 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 (
+
+ );
+};
+
+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
+
+ {DUMMY_PRODUCTS.map((product) => (
+
+ ))}
+
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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