├── 01_lesson
├── .gitignore
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── App.js
│ ├── app
│ └── store.js
│ ├── features
│ └── counter
│ │ ├── Counter.js
│ │ └── counterSlice.js
│ ├── index.css
│ └── index.js
├── 02_lesson
├── .gitignore
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── App.js
│ ├── app
│ └── store.js
│ ├── features
│ ├── posts
│ │ ├── AddPostForm.js
│ │ ├── PostAuthor.js
│ │ ├── PostsList.js
│ │ ├── ReactionButtons.js
│ │ ├── TimeAgo.js
│ │ └── postsSlice.js
│ └── users
│ │ └── usersSlice.js
│ ├── index.css
│ └── index.js
├── 02_lesson_starter
├── .gitignore
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── App.js
│ ├── app
│ └── store.js
│ ├── index.css
│ └── index.js
├── 03_lesson
├── .gitignore
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── App.js
│ ├── app
│ └── store.js
│ ├── features
│ ├── posts
│ │ ├── AddPostForm.js
│ │ ├── PostAuthor.js
│ │ ├── PostsExcerpt.js
│ │ ├── PostsList.js
│ │ ├── ReactionButtons.js
│ │ ├── TimeAgo.js
│ │ └── postsSlice.js
│ └── users
│ │ └── usersSlice.js
│ ├── index.css
│ └── index.js
├── 03_lesson_starter
├── .gitignore
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── App.js
│ ├── app
│ └── store.js
│ ├── features
│ ├── posts
│ │ ├── AddPostForm.js
│ │ ├── PostAuthor.js
│ │ ├── PostsList.js
│ │ ├── ReactionButtons.js
│ │ ├── TimeAgo.js
│ │ └── postsSlice.js
│ └── users
│ │ └── usersSlice.js
│ ├── index.css
│ └── index.js
├── 04_lesson
├── .gitignore
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── App.js
│ ├── app
│ └── store.js
│ ├── components
│ ├── Header.js
│ └── Layout.js
│ ├── features
│ ├── posts
│ │ ├── AddPostForm.js
│ │ ├── EditPostForm.js
│ │ ├── PostAuthor.js
│ │ ├── PostsExcerpt.js
│ │ ├── PostsList.js
│ │ ├── ReactionButtons.js
│ │ ├── SinglePostPage.js
│ │ ├── TimeAgo.js
│ │ └── postsSlice.js
│ └── users
│ │ └── usersSlice.js
│ ├── index.css
│ └── index.js
├── 04_lesson_starter
├── .gitignore
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── App.js
│ ├── app
│ └── store.js
│ ├── features
│ ├── posts
│ │ ├── AddPostForm.js
│ │ ├── PostAuthor.js
│ │ ├── PostsExcerpt.js
│ │ ├── PostsList.js
│ │ ├── ReactionButtons.js
│ │ ├── TimeAgo.js
│ │ └── postsSlice.js
│ └── users
│ │ └── usersSlice.js
│ ├── index.css
│ └── index.js
├── 05_lesson
├── .gitignore
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── App.js
│ ├── app
│ └── store.js
│ ├── components
│ ├── Header.js
│ └── Layout.js
│ ├── features
│ ├── posts
│ │ ├── AddPostForm.js
│ │ ├── EditPostForm.js
│ │ ├── PostAuthor.js
│ │ ├── PostsExcerpt.js
│ │ ├── PostsList.js
│ │ ├── ReactionButtons.js
│ │ ├── SinglePostPage.js
│ │ ├── TimeAgo.js
│ │ └── postsSlice.js
│ └── users
│ │ ├── UserPage.js
│ │ ├── UsersList.js
│ │ └── usersSlice.js
│ ├── index.css
│ └── index.js
├── 05_lesson_starter
├── .gitignore
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── App.js
│ ├── app
│ └── store.js
│ ├── components
│ ├── Header.js
│ └── Layout.js
│ ├── features
│ ├── posts
│ │ ├── AddPostForm.js
│ │ ├── EditPostForm.js
│ │ ├── PostAuthor.js
│ │ ├── PostsExcerpt.js
│ │ ├── PostsList.js
│ │ ├── ReactionButtons.js
│ │ ├── SinglePostPage.js
│ │ ├── TimeAgo.js
│ │ └── postsSlice.js
│ └── users
│ │ └── usersSlice.js
│ ├── index.css
│ └── index.js
├── 06_lesson
├── .gitignore
├── data
│ └── db.json
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── App.js
│ ├── features
│ ├── api
│ │ └── apiSlice.js
│ └── todos
│ │ └── TodoList.js
│ ├── index.css
│ └── index.js
├── 06_lesson_starter
├── .gitignore
├── data
│ └── db.json
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── App.js
│ ├── features
│ └── todos
│ │ └── TodoList.js
│ ├── index.css
│ └── index.js
├── 07_lesson
├── .gitignore
├── data
│ └── db.json
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── App.js
│ ├── app
│ └── store.js
│ ├── components
│ ├── Header.js
│ └── Layout.js
│ ├── features
│ ├── api
│ │ └── apiSlice.js
│ ├── posts
│ │ ├── AddPostForm.js
│ │ ├── EditPostForm.js
│ │ ├── PostAuthor.js
│ │ ├── PostsExcerpt.js
│ │ ├── PostsList.js
│ │ ├── ReactionButtons.js
│ │ ├── SinglePostPage.js
│ │ ├── TimeAgo.js
│ │ └── postsSlice.js
│ └── users
│ │ ├── UserPage.js
│ │ ├── UsersList.js
│ │ └── usersSlice.js
│ ├── index.css
│ └── index.js
├── 07_lesson_starter
├── .gitignore
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── App.js
│ ├── app
│ └── store.js
│ ├── components
│ ├── Header.js
│ └── Layout.js
│ ├── features
│ ├── posts
│ │ ├── AddPostForm.js
│ │ ├── EditPostForm.js
│ │ ├── PostAuthor.js
│ │ ├── PostsExcerpt.js
│ │ ├── PostsList.js
│ │ ├── ReactionButtons.js
│ │ ├── SinglePostPage.js
│ │ ├── TimeAgo.js
│ │ └── postsSlice.js
│ └── users
│ │ ├── UserPage.js
│ │ ├── UsersList.js
│ │ └── usersSlice.js
│ ├── index.css
│ └── index.js
├── 08_lesson
├── .gitignore
├── README.md
├── data
│ └── db.json
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── App.js
│ ├── app
│ └── store.js
│ ├── components
│ ├── Header.js
│ └── Layout.js
│ ├── features
│ ├── api
│ │ └── apiSlice.js
│ ├── posts
│ │ ├── AddPostForm.js
│ │ ├── EditPostForm.js
│ │ ├── PostAuthor.js
│ │ ├── PostsExcerpt.js
│ │ ├── PostsList.js
│ │ ├── ReactionButtons.js
│ │ ├── SinglePostPage.js
│ │ ├── TimeAgo.js
│ │ └── postsSlice.js
│ └── users
│ │ ├── UserPage.js
│ │ ├── UsersList.js
│ │ └── usersSlice.js
│ ├── index.css
│ └── index.js
└── README.md
/01_lesson/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/01_lesson/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "01_lesson",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reduxjs/toolkit": "^1.8.0",
7 | "react": "^17.0.2",
8 | "react-dom": "^17.0.2",
9 | "react-redux": "^7.2.6",
10 | "react-scripts": "5.0.0"
11 | },
12 | "scripts": {
13 | "start": "react-scripts start",
14 | "build": "react-scripts build",
15 | "eject": "react-scripts eject"
16 | },
17 | "eslintConfig": {
18 | "extends": [
19 | "react-app",
20 | "react-app/jest"
21 | ]
22 | },
23 | "browserslist": {
24 | "production": [
25 | ">0.2%",
26 | "not dead",
27 | "not op_mini all"
28 | ],
29 | "development": [
30 | "last 1 chrome version",
31 | "last 1 firefox version",
32 | "last 1 safari version"
33 | ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/01_lesson/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/01_lesson/public/favicon.ico
--------------------------------------------------------------------------------
/01_lesson/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/01_lesson/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/01_lesson/public/logo192.png
--------------------------------------------------------------------------------
/01_lesson/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/01_lesson/public/logo512.png
--------------------------------------------------------------------------------
/01_lesson/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/01_lesson/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/01_lesson/src/App.js:
--------------------------------------------------------------------------------
1 | import Counter from "./features/counter/Counter";
2 |
3 | function App() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
11 | export default App;
12 |
--------------------------------------------------------------------------------
/01_lesson/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import counterReducer from '../features/counter/counterSlice';
3 |
4 | export const store = configureStore({
5 | reducer: {
6 | counter: counterReducer,
7 | }
8 | })
--------------------------------------------------------------------------------
/01_lesson/src/features/counter/Counter.js:
--------------------------------------------------------------------------------
1 | import { useSelector, useDispatch } from "react-redux";
2 | import {
3 | increment,
4 | decrement,
5 | reset,
6 | incrementByAmount
7 | } from './counterSlice';
8 | import { useState } from "react";
9 |
10 | const Counter = () => {
11 | const count = useSelector((state) => state.counter.count);
12 | const dispatch = useDispatch();
13 |
14 | const [incrementAmount, setIncrementAmount] = useState(0);
15 |
16 | const addValue = Number(incrementAmount) || 0;
17 |
18 | const resetAll = () => {
19 | setIncrementAmount(0);
20 | dispatch(reset());
21 | }
22 |
23 | return (
24 |
25 | {count}
26 |
27 |
28 |
29 |
30 |
31 | setIncrementAmount(e.target.value)}
35 | />
36 |
37 |
38 |
39 |
40 |
41 |
42 | )
43 | }
44 | export default Counter
--------------------------------------------------------------------------------
/01_lesson/src/features/counter/counterSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | count: 0
5 | }
6 |
7 | export const counterSlice = createSlice({
8 | name: 'counter',
9 | initialState,
10 | reducers: {
11 | increment: (state) => {
12 | state.count += 1;
13 | },
14 | decrement: (state) => {
15 | state.count -= 1;
16 | },
17 | reset: (state) => {
18 | state.count = 0;
19 | },
20 | incrementByAmount: (state, action) => {
21 | state.count += action.payload;
22 | }
23 | }
24 | });
25 |
26 | export const { increment, decrement, reset, incrementByAmount } = counterSlice.actions;
27 |
28 | export default counterSlice.reducer;
--------------------------------------------------------------------------------
/01_lesson/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | body {
8 | min-height: 100vh;
9 | font-size: 5rem;
10 | display: grid;
11 | place-content: center;
12 | }
13 |
14 | input,
15 | button {
16 | font: inherit;
17 | padding: 0.5rem;
18 | }
19 |
20 | section {
21 | display: flex;
22 | flex-direction: column;
23 | justify-content: center;
24 | align-items: center;
25 | }
26 |
27 | input {
28 | text-align: center;
29 | max-width: 2.5em;
30 | }
31 |
32 | button {
33 | font-size: 2rem;
34 | margin: 0.5em 0 0.5em 0.5em;
35 | min-width: 2em;
36 | }
37 |
38 | button:first-child {
39 | margin-left: 0;
40 | }
41 |
--------------------------------------------------------------------------------
/01_lesson/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import { store } from './app/store';
6 | import { Provider } from 'react-redux';
7 |
8 | ReactDOM.render(
9 |
10 |
11 |
12 |
13 | ,
14 | document.getElementById('root')
15 | );
16 |
--------------------------------------------------------------------------------
/02_lesson/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/02_lesson/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "02_lesson",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reduxjs/toolkit": "^1.8.0",
7 | "date-fns": "^2.28.0",
8 | "react": "^17.0.2",
9 | "react-dom": "^17.0.2",
10 | "react-redux": "^7.2.6",
11 | "react-scripts": "5.0.0"
12 | },
13 | "scripts": {
14 | "start": "react-scripts start",
15 | "build": "react-scripts build",
16 | "eject": "react-scripts eject"
17 | },
18 | "eslintConfig": {
19 | "extends": [
20 | "react-app",
21 | "react-app/jest"
22 | ]
23 | },
24 | "browserslist": {
25 | "production": [
26 | ">0.2%",
27 | "not dead",
28 | "not op_mini all"
29 | ],
30 | "development": [
31 | "last 1 chrome version",
32 | "last 1 firefox version",
33 | "last 1 safari version"
34 | ]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/02_lesson/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/02_lesson/public/favicon.ico
--------------------------------------------------------------------------------
/02_lesson/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/02_lesson/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/02_lesson/public/logo192.png
--------------------------------------------------------------------------------
/02_lesson/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/02_lesson/public/logo512.png
--------------------------------------------------------------------------------
/02_lesson/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/02_lesson/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/02_lesson/src/App.js:
--------------------------------------------------------------------------------
1 | import PostsList from "./features/posts/PostsList";
2 | import AddPostForm from "./features/posts/AddPostForm";
3 |
4 | function App() {
5 | return (
6 |
7 |
8 |
9 |
10 | );
11 | }
12 |
13 | export default App;
14 |
--------------------------------------------------------------------------------
/02_lesson/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import postsReducer from '../features/posts/postsSlice';
3 | import usersReducer from '../features/users/usersSlice';
4 |
5 |
6 | export const store = configureStore({
7 | reducer: {
8 | posts: postsReducer,
9 | users: usersReducer
10 | }
11 | })
--------------------------------------------------------------------------------
/02_lesson/src/features/posts/AddPostForm.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 |
4 | import { postAdded } from "./postsSlice";
5 | import { selectAllUsers } from "../users/usersSlice";
6 |
7 | const AddPostForm = () => {
8 | const dispatch = useDispatch()
9 |
10 | const [title, setTitle] = useState('')
11 | const [content, setContent] = useState('')
12 | const [userId, setUserId] = useState('')
13 |
14 | const users = useSelector(selectAllUsers)
15 |
16 | const onTitleChanged = e => setTitle(e.target.value)
17 | const onContentChanged = e => setContent(e.target.value)
18 | const onAuthorChanged = e => setUserId(e.target.value)
19 |
20 | const onSavePostClicked = () => {
21 | if (title && content) {
22 | dispatch(
23 | postAdded(title, content, userId)
24 | )
25 | setTitle('')
26 | setContent('')
27 | }
28 | }
29 |
30 | const canSave = Boolean(title) && Boolean(content) && Boolean(userId)
31 |
32 | const usersOptions = users.map(user => (
33 |
36 | ))
37 |
38 | return (
39 |
40 | Add a New Post
41 |
68 |
69 | )
70 | }
71 | export default AddPostForm
--------------------------------------------------------------------------------
/02_lesson/src/features/posts/PostAuthor.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectAllUsers } from "../users/usersSlice";
3 |
4 | const PostAuthor = ({ userId }) => {
5 | const users = useSelector(selectAllUsers)
6 |
7 | const author = users.find(user => user.id === userId);
8 |
9 | return by {author ? author.name : 'Unknown author'}
10 | }
11 | export default PostAuthor
--------------------------------------------------------------------------------
/02_lesson/src/features/posts/PostsList.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectAllPosts } from "./postsSlice";
3 | import PostAuthor from "./PostAuthor";
4 | import TimeAgo from "./TimeAgo";
5 | import ReactionButtons from "./ReactionButtons";
6 |
7 | const PostsList = () => {
8 | const posts = useSelector(selectAllPosts)
9 |
10 | const orderedPosts = posts.slice().sort((a, b) => b.date.localeCompare(a.date))
11 |
12 | const renderedPosts = orderedPosts.map(post => (
13 |
14 | {post.title}
15 | {post.content.substring(0, 100)}
16 |
17 |
18 |
19 |
20 |
21 |
22 | ))
23 |
24 | return (
25 |
26 | Posts
27 | {renderedPosts}
28 |
29 | )
30 | }
31 | export default PostsList
--------------------------------------------------------------------------------
/02_lesson/src/features/posts/ReactionButtons.js:
--------------------------------------------------------------------------------
1 | import { useDispatch } from "react-redux";
2 | import { reactionAdded } from "./postsSlice";
3 |
4 | const reactionEmoji = {
5 | thumbsUp: '👍',
6 | wow: '😮',
7 | heart: '❤️',
8 | rocket: '🚀',
9 | coffee: '☕'
10 | }
11 |
12 | const ReactionButtons = ({ post }) => {
13 | const dispatch = useDispatch()
14 |
15 | const reactionButtons = Object.entries(reactionEmoji).map(([name, emoji]) => {
16 | return (
17 |
27 | )
28 | })
29 |
30 | return {reactionButtons}
31 | }
32 | export default ReactionButtons
--------------------------------------------------------------------------------
/02_lesson/src/features/posts/TimeAgo.js:
--------------------------------------------------------------------------------
1 | import { parseISO, formatDistanceToNow } from 'date-fns';
2 |
3 | const TimeAgo = ({ timestamp }) => {
4 | let timeAgo = ''
5 | if (timestamp) {
6 | const date = parseISO(timestamp)
7 | const timePeriod = formatDistanceToNow(date)
8 | timeAgo = `${timePeriod} ago`
9 | }
10 |
11 | return (
12 |
13 | {timeAgo}
14 |
15 | )
16 | }
17 | export default TimeAgo
--------------------------------------------------------------------------------
/02_lesson/src/features/posts/postsSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, nanoid } from "@reduxjs/toolkit";
2 | import { sub } from 'date-fns';
3 |
4 | const initialState = [
5 | {
6 | id: '1',
7 | title: 'Learning Redux Toolkit',
8 | content: "I've heard good things.",
9 | date: sub(new Date(), { minutes: 10 }).toISOString(),
10 | reactions: {
11 | thumbsUp: 0,
12 | wow: 0,
13 | heart: 0,
14 | rocket: 0,
15 | coffee: 0
16 | }
17 | },
18 | {
19 | id: '2',
20 | title: 'Slices...',
21 | content: "The more I say slice, the more I want pizza.",
22 | date: sub(new Date(), { minutes: 5 }).toISOString(),
23 | reactions: {
24 | thumbsUp: 0,
25 | wow: 0,
26 | heart: 0,
27 | rocket: 0,
28 | coffee: 0
29 | }
30 | }
31 | ]
32 |
33 | const postsSlice = createSlice({
34 | name: 'posts',
35 | initialState,
36 | reducers: {
37 | postAdded: {
38 | reducer(state, action) {
39 | state.push(action.payload)
40 | },
41 | prepare(title, content, userId) {
42 | return {
43 | payload: {
44 | id: nanoid(),
45 | title,
46 | content,
47 | date: new Date().toISOString(),
48 | userId,
49 | reactions: {
50 | thumbsUp: 0,
51 | wow: 0,
52 | heart: 0,
53 | rocket: 0,
54 | coffee: 0
55 | }
56 | }
57 | }
58 | }
59 | },
60 | reactionAdded(state, action) {
61 | const { postId, reaction } = action.payload
62 | const existingPost = state.find(post => post.id === postId)
63 | if (existingPost) {
64 | existingPost.reactions[reaction]++
65 | }
66 | }
67 | }
68 | })
69 |
70 | export const selectAllPosts = (state) => state.posts;
71 |
72 | export const { postAdded, reactionAdded } = postsSlice.actions
73 |
74 | export default postsSlice.reducer
--------------------------------------------------------------------------------
/02_lesson/src/features/users/usersSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = [
4 | { id: '0', name: 'Dude Lebowski' },
5 | { id: '1', name: 'Neil Young' },
6 | { id: '2', name: 'Dave Gray' }
7 | ]
8 |
9 | const usersSlice = createSlice({
10 | name: 'users',
11 | initialState,
12 | reducers: {}
13 | })
14 |
15 | export const selectAllUsers = (state) => state.users;
16 |
17 | export default usersSlice.reducer
--------------------------------------------------------------------------------
/02_lesson/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | html {
8 | font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
9 | background: #333;
10 | color: whitesmoke;
11 | }
12 |
13 | body {
14 | min-height: 100vh;
15 | font-size: 1.5rem;
16 | padding: 0 10% 10%;
17 | }
18 |
19 | input,
20 | textarea,
21 | button,
22 | select {
23 | font: inherit;
24 | margin-bottom: 1em;
25 | }
26 |
27 | main {
28 | max-width: 500px;
29 | margin: auto;
30 | }
31 |
32 | section {
33 | margin-top: 1em;
34 | }
35 |
36 | article {
37 | margin: 0.5em 0.5em 0.5em 0;
38 | border: 1px solid whitesmoke;
39 | border-radius: 10px;
40 | padding: 1em;
41 | }
42 |
43 | h1 {
44 | font-size: 3.5rem;
45 | }
46 |
47 | p {
48 | font-family: Arial, Helvetica, sans-serif;
49 | line-height: 1.4;
50 | font-size: 1.2rem;
51 | margin: 0.5em 0;
52 | }
53 |
54 | form {
55 | display: flex;
56 | flex-direction: column;
57 | }
58 |
59 | .postCredit {
60 | font-size: 1rem;
61 | }
62 |
63 | .reactionButton {
64 | margin: 0 0.25em 0 0;
65 | background: transparent;
66 | border: none;
67 | color: whitesmoke;
68 | font-size: 1rem;
69 | }
70 |
--------------------------------------------------------------------------------
/02_lesson/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import { store } from './app/store';
6 | import { Provider } from 'react-redux';
7 |
8 | ReactDOM.render(
9 |
10 |
11 |
12 |
13 | ,
14 | document.getElementById('root')
15 | );
16 |
--------------------------------------------------------------------------------
/02_lesson_starter/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/02_lesson_starter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "02_lesson",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reduxjs/toolkit": "^1.8.0",
7 | "react": "^17.0.2",
8 | "react-dom": "^17.0.2",
9 | "react-redux": "^7.2.6",
10 | "react-scripts": "5.0.0"
11 | },
12 | "scripts": {
13 | "start": "react-scripts start",
14 | "build": "react-scripts build",
15 | "eject": "react-scripts eject"
16 | },
17 | "eslintConfig": {
18 | "extends": [
19 | "react-app",
20 | "react-app/jest"
21 | ]
22 | },
23 | "browserslist": {
24 | "production": [
25 | ">0.2%",
26 | "not dead",
27 | "not op_mini all"
28 | ],
29 | "development": [
30 | "last 1 chrome version",
31 | "last 1 firefox version",
32 | "last 1 safari version"
33 | ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/02_lesson_starter/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/02_lesson_starter/public/favicon.ico
--------------------------------------------------------------------------------
/02_lesson_starter/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/02_lesson_starter/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/02_lesson_starter/public/logo192.png
--------------------------------------------------------------------------------
/02_lesson_starter/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/02_lesson_starter/public/logo512.png
--------------------------------------------------------------------------------
/02_lesson_starter/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/02_lesson_starter/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/02_lesson_starter/src/App.js:
--------------------------------------------------------------------------------
1 |
2 | function App() {
3 | return (
4 |
5 |
6 |
7 | );
8 | }
9 |
10 | export default App;
11 |
--------------------------------------------------------------------------------
/02_lesson_starter/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 |
3 |
4 | export const store = configureStore({
5 | reducer: {
6 |
7 | }
8 | })
--------------------------------------------------------------------------------
/02_lesson_starter/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | html {
8 | font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
9 | background: #333;
10 | color: whitesmoke;
11 | }
12 |
13 | body {
14 | min-height: 100vh;
15 | font-size: 1.5rem;
16 | padding: 0 10% 10%;
17 | }
18 |
19 | input,
20 | textarea,
21 | button,
22 | select {
23 | font: inherit;
24 | margin-bottom: 1em;
25 | }
26 |
27 | main {
28 | max-width: 500px;
29 | margin: auto;
30 | }
31 |
32 | section {
33 | margin-top: 1em;
34 | }
35 |
36 | article {
37 | margin: 0.5em 0.5em 0.5em 0;
38 | border: 1px solid whitesmoke;
39 | border-radius: 10px;
40 | padding: 1em;
41 | }
42 |
43 | h1 {
44 | font-size: 3.5rem;
45 | }
46 |
47 | p {
48 | font-family: Arial, Helvetica, sans-serif;
49 | line-height: 1.4;
50 | font-size: 1.2rem;
51 | margin: 0.5em 0;
52 | }
53 |
54 | form {
55 | display: flex;
56 | flex-direction: column;
57 | }
58 |
59 | .postCredit {
60 | font-size: 1rem;
61 | }
62 |
63 | .reactionButton {
64 | margin: 0 0.25em 0 0;
65 | background: transparent;
66 | border: none;
67 | color: whitesmoke;
68 | font-size: 1rem;
69 | }
70 |
--------------------------------------------------------------------------------
/02_lesson_starter/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import { store } from './app/store';
6 | import { Provider } from 'react-redux';
7 |
8 | ReactDOM.render(
9 |
10 |
11 |
12 |
13 | ,
14 | document.getElementById('root')
15 | );
16 |
--------------------------------------------------------------------------------
/03_lesson/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/03_lesson/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "03_lesson",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reduxjs/toolkit": "^1.8.0",
7 | "axios": "^0.26.1",
8 | "date-fns": "^2.28.0",
9 | "react": "^17.0.2",
10 | "react-dom": "^17.0.2",
11 | "react-redux": "^7.2.6",
12 | "react-scripts": "5.0.0"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "eject": "react-scripts eject"
18 | },
19 | "eslintConfig": {
20 | "extends": [
21 | "react-app",
22 | "react-app/jest"
23 | ]
24 | },
25 | "browserslist": {
26 | "production": [
27 | ">0.2%",
28 | "not dead",
29 | "not op_mini all"
30 | ],
31 | "development": [
32 | "last 1 chrome version",
33 | "last 1 firefox version",
34 | "last 1 safari version"
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/03_lesson/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/03_lesson/public/favicon.ico
--------------------------------------------------------------------------------
/03_lesson/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/03_lesson/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/03_lesson/public/logo192.png
--------------------------------------------------------------------------------
/03_lesson/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/03_lesson/public/logo512.png
--------------------------------------------------------------------------------
/03_lesson/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/03_lesson/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/03_lesson/src/App.js:
--------------------------------------------------------------------------------
1 | import PostsList from "./features/posts/PostsList";
2 | import AddPostForm from "./features/posts/AddPostForm";
3 |
4 | function App() {
5 | return (
6 |
7 |
8 |
9 |
10 | );
11 | }
12 |
13 | export default App;
14 |
--------------------------------------------------------------------------------
/03_lesson/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import postsReducer from '../features/posts/postsSlice';
3 | import usersReducer from '../features/users/usersSlice';
4 |
5 |
6 | export const store = configureStore({
7 | reducer: {
8 | posts: postsReducer,
9 | users: usersReducer
10 | }
11 | })
--------------------------------------------------------------------------------
/03_lesson/src/features/posts/PostAuthor.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectAllUsers } from "../users/usersSlice";
3 |
4 | const PostAuthor = ({ userId }) => {
5 | const users = useSelector(selectAllUsers)
6 |
7 | const author = users.find(user => user.id === userId);
8 |
9 | return by {author ? author.name : 'Unknown author'}
10 | }
11 | export default PostAuthor
--------------------------------------------------------------------------------
/03_lesson/src/features/posts/PostsExcerpt.js:
--------------------------------------------------------------------------------
1 | import PostAuthor from "./PostAuthor";
2 | import TimeAgo from "./TimeAgo";
3 | import ReactionButtons from "./ReactionButtons";
4 |
5 | const PostsExcerpt = ({ post }) => {
6 | return (
7 |
8 | {post.title}
9 | {post.body.substring(0, 100)}
10 |
11 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 | export default PostsExcerpt
--------------------------------------------------------------------------------
/03_lesson/src/features/posts/PostsList.js:
--------------------------------------------------------------------------------
1 | import { useSelector, useDispatch } from "react-redux";
2 | import { selectAllPosts, getPostsStatus, getPostsError, fetchPosts } from "./postsSlice";
3 | import { useEffect } from "react";
4 | import PostsExcerpt from "./PostsExcerpt";
5 |
6 | const PostsList = () => {
7 | const dispatch = useDispatch();
8 |
9 | const posts = useSelector(selectAllPosts);
10 | const postStatus = useSelector(getPostsStatus);
11 | const error = useSelector(getPostsError);
12 |
13 | useEffect(() => {
14 | if (postStatus === 'idle') {
15 | dispatch(fetchPosts())
16 | }
17 | }, [postStatus, dispatch])
18 |
19 | let content;
20 | if (postStatus === 'loading') {
21 | content = "Loading..."
;
22 | } else if (postStatus === 'succeeded') {
23 | const orderedPosts = posts.slice().sort((a, b) => b.date.localeCompare(a.date))
24 | content = orderedPosts.map(post => )
25 | } else if (postStatus === 'failed') {
26 | content = {error}
;
27 | }
28 |
29 | return (
30 |
31 | Posts
32 | {content}
33 |
34 | )
35 | }
36 | export default PostsList
--------------------------------------------------------------------------------
/03_lesson/src/features/posts/ReactionButtons.js:
--------------------------------------------------------------------------------
1 | import { useDispatch } from "react-redux";
2 | import { reactionAdded } from "./postsSlice";
3 |
4 | const reactionEmoji = {
5 | thumbsUp: '👍',
6 | wow: '😮',
7 | heart: '❤️',
8 | rocket: '🚀',
9 | coffee: '☕'
10 | }
11 |
12 | const ReactionButtons = ({ post }) => {
13 | const dispatch = useDispatch()
14 |
15 | const reactionButtons = Object.entries(reactionEmoji).map(([name, emoji]) => {
16 | return (
17 |
27 | )
28 | })
29 |
30 | return {reactionButtons}
31 | }
32 | export default ReactionButtons
--------------------------------------------------------------------------------
/03_lesson/src/features/posts/TimeAgo.js:
--------------------------------------------------------------------------------
1 | import { parseISO, formatDistanceToNow } from 'date-fns';
2 |
3 | const TimeAgo = ({ timestamp }) => {
4 | let timeAgo = ''
5 | if (timestamp) {
6 | const date = parseISO(timestamp)
7 | const timePeriod = formatDistanceToNow(date)
8 | timeAgo = `${timePeriod} ago`
9 | }
10 |
11 | return (
12 |
13 | {timeAgo}
14 |
15 | )
16 | }
17 | export default TimeAgo
--------------------------------------------------------------------------------
/03_lesson/src/features/users/usersSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
2 | import axios from "axios";
3 |
4 | const USERS_URL = 'https://jsonplaceholder.typicode.com/users';
5 |
6 | const initialState = []
7 |
8 | export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
9 | const response = await axios.get(USERS_URL);
10 | return response.data
11 | })
12 |
13 | const usersSlice = createSlice({
14 | name: 'users',
15 | initialState,
16 | reducers: {},
17 | extraReducers(builder) {
18 | builder.addCase(fetchUsers.fulfilled, (state, action) => {
19 | return action.payload;
20 | })
21 | }
22 | })
23 |
24 | export const selectAllUsers = (state) => state.users;
25 |
26 | export default usersSlice.reducer
--------------------------------------------------------------------------------
/03_lesson/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | html {
8 | font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
9 | background: #333;
10 | color: whitesmoke;
11 | }
12 |
13 | body {
14 | min-height: 100vh;
15 | font-size: 1.5rem;
16 | padding: 0 10% 10%;
17 | }
18 |
19 | input,
20 | textarea,
21 | button,
22 | select {
23 | font: inherit;
24 | margin-bottom: 1em;
25 | }
26 |
27 | main {
28 | max-width: 500px;
29 | margin: auto;
30 | }
31 |
32 | section {
33 | margin-top: 1em;
34 | }
35 |
36 | article {
37 | margin: 0.5em 0.5em 0.5em 0;
38 | border: 1px solid whitesmoke;
39 | border-radius: 10px;
40 | padding: 1em;
41 | }
42 |
43 | h1 {
44 | font-size: 3.5rem;
45 | }
46 |
47 | p {
48 | font-family: Arial, Helvetica, sans-serif;
49 | line-height: 1.4;
50 | font-size: 1.2rem;
51 | margin: 0.5em 0;
52 | }
53 |
54 | form {
55 | display: flex;
56 | flex-direction: column;
57 | }
58 |
59 | .postCredit {
60 | font-size: 1rem;
61 | }
62 |
63 | .reactionButton {
64 | margin: 0 0.25em 0 0;
65 | background: transparent;
66 | border: none;
67 | color: whitesmoke;
68 | font-size: 1rem;
69 | }
70 |
--------------------------------------------------------------------------------
/03_lesson/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import { store } from './app/store';
6 | import { Provider } from 'react-redux';
7 | import { fetchUsers } from './features/users/usersSlice';
8 |
9 | store.dispatch(fetchUsers());
10 |
11 | ReactDOM.render(
12 |
13 |
14 |
15 |
16 | ,
17 | document.getElementById('root')
18 | );
19 |
--------------------------------------------------------------------------------
/03_lesson_starter/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/03_lesson_starter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "03_lesson",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reduxjs/toolkit": "^1.8.0",
7 | "date-fns": "^2.28.0",
8 | "react": "^17.0.2",
9 | "react-dom": "^17.0.2",
10 | "react-redux": "^7.2.6",
11 | "react-scripts": "5.0.0"
12 | },
13 | "scripts": {
14 | "start": "react-scripts start",
15 | "build": "react-scripts build",
16 | "eject": "react-scripts eject"
17 | },
18 | "eslintConfig": {
19 | "extends": [
20 | "react-app",
21 | "react-app/jest"
22 | ]
23 | },
24 | "browserslist": {
25 | "production": [
26 | ">0.2%",
27 | "not dead",
28 | "not op_mini all"
29 | ],
30 | "development": [
31 | "last 1 chrome version",
32 | "last 1 firefox version",
33 | "last 1 safari version"
34 | ]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/03_lesson_starter/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/03_lesson_starter/public/favicon.ico
--------------------------------------------------------------------------------
/03_lesson_starter/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/03_lesson_starter/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/03_lesson_starter/public/logo192.png
--------------------------------------------------------------------------------
/03_lesson_starter/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/03_lesson_starter/public/logo512.png
--------------------------------------------------------------------------------
/03_lesson_starter/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/03_lesson_starter/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/03_lesson_starter/src/App.js:
--------------------------------------------------------------------------------
1 | import PostsList from "./features/posts/PostsList";
2 | import AddPostForm from "./features/posts/AddPostForm";
3 |
4 | function App() {
5 | return (
6 |
7 |
8 |
9 |
10 | );
11 | }
12 |
13 | export default App;
14 |
--------------------------------------------------------------------------------
/03_lesson_starter/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import postsReducer from '../features/posts/postsSlice';
3 | import usersReducer from '../features/users/usersSlice';
4 |
5 |
6 | export const store = configureStore({
7 | reducer: {
8 | posts: postsReducer,
9 | users: usersReducer
10 | }
11 | })
--------------------------------------------------------------------------------
/03_lesson_starter/src/features/posts/AddPostForm.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 |
4 | import { postAdded } from "./postsSlice";
5 | import { selectAllUsers } from "../users/usersSlice";
6 |
7 | const AddPostForm = () => {
8 | const dispatch = useDispatch()
9 |
10 | const [title, setTitle] = useState('')
11 | const [content, setContent] = useState('')
12 | const [userId, setUserId] = useState('')
13 |
14 | const users = useSelector(selectAllUsers)
15 |
16 | const onTitleChanged = e => setTitle(e.target.value)
17 | const onContentChanged = e => setContent(e.target.value)
18 | const onAuthorChanged = e => setUserId(e.target.value)
19 |
20 | const onSavePostClicked = () => {
21 | if (title && content) {
22 | dispatch(
23 | postAdded(title, content, userId)
24 | )
25 | setTitle('')
26 | setContent('')
27 | }
28 | }
29 |
30 | const canSave = Boolean(title) && Boolean(content) && Boolean(userId)
31 |
32 | const usersOptions = users.map(user => (
33 |
36 | ))
37 |
38 | return (
39 |
69 | )
70 | }
71 | export default AddPostForm
--------------------------------------------------------------------------------
/03_lesson_starter/src/features/posts/PostAuthor.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectAllUsers } from "../users/usersSlice";
3 |
4 | const PostAuthor = ({ userId }) => {
5 | const users = useSelector(selectAllUsers)
6 |
7 | const author = users.find(user => user.id === userId);
8 |
9 | return by {author ? author.name : 'Unknown author'}
10 | }
11 | export default PostAuthor
--------------------------------------------------------------------------------
/03_lesson_starter/src/features/posts/PostsList.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectAllPosts } from "./postsSlice";
3 | import PostAuthor from "./PostAuthor";
4 | import TimeAgo from "./TimeAgo";
5 | import ReactionButtons from "./ReactionButtons";
6 |
7 | const PostsList = () => {
8 | const posts = useSelector(selectAllPosts)
9 |
10 | const orderedPosts = posts.slice().sort((a, b) => b.date.localeCompare(a.date))
11 |
12 | const renderedPosts = orderedPosts.map(post => (
13 |
14 | {post.title}
15 | {post.content.substring(0, 100)}
16 |
17 |
18 |
19 |
20 |
21 |
22 | ))
23 |
24 | return (
25 |
26 | Posts
27 | {renderedPosts}
28 |
29 | )
30 | }
31 | export default PostsList
--------------------------------------------------------------------------------
/03_lesson_starter/src/features/posts/ReactionButtons.js:
--------------------------------------------------------------------------------
1 | import { useDispatch } from "react-redux";
2 | import { reactionAdded } from "./postsSlice";
3 |
4 | const reactionEmoji = {
5 | thumbsUp: '👍',
6 | wow: '😮',
7 | heart: '❤️',
8 | rocket: '🚀',
9 | coffee: '☕'
10 | }
11 |
12 | const ReactionButtons = ({ post }) => {
13 | const dispatch = useDispatch()
14 |
15 | const reactionButtons = Object.entries(reactionEmoji).map(([name, emoji]) => {
16 | return (
17 |
27 | )
28 | })
29 |
30 | return {reactionButtons}
31 | }
32 | export default ReactionButtons
--------------------------------------------------------------------------------
/03_lesson_starter/src/features/posts/TimeAgo.js:
--------------------------------------------------------------------------------
1 | import { parseISO, formatDistanceToNow } from 'date-fns';
2 |
3 | const TimeAgo = ({ timestamp }) => {
4 | let timeAgo = ''
5 | if (timestamp) {
6 | const date = parseISO(timestamp)
7 | const timePeriod = formatDistanceToNow(date)
8 | timeAgo = `${timePeriod} ago`
9 | }
10 |
11 | return (
12 |
13 | {timeAgo}
14 |
15 | )
16 | }
17 | export default TimeAgo
--------------------------------------------------------------------------------
/03_lesson_starter/src/features/posts/postsSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, nanoid } from "@reduxjs/toolkit";
2 | import { sub } from 'date-fns';
3 |
4 | const initialState = [
5 | {
6 | id: '1',
7 | title: 'Learning Redux Toolkit',
8 | content: "I've heard good things.",
9 | date: sub(new Date(), { minutes: 10 }).toISOString(),
10 | reactions: {
11 | thumbsUp: 0,
12 | wow: 0,
13 | heart: 0,
14 | rocket: 0,
15 | coffee: 0
16 | }
17 | },
18 | {
19 | id: '2',
20 | title: 'Slices...',
21 | content: "The more I say slice, the more I want pizza.",
22 | date: sub(new Date(), { minutes: 5 }).toISOString(),
23 | reactions: {
24 | thumbsUp: 0,
25 | wow: 0,
26 | heart: 0,
27 | rocket: 0,
28 | coffee: 0
29 | }
30 | }
31 | ]
32 |
33 | const postsSlice = createSlice({
34 | name: 'posts',
35 | initialState,
36 | reducers: {
37 | postAdded: {
38 | reducer(state, action) {
39 | state.push(action.payload)
40 | },
41 | prepare(title, content, userId) {
42 | return {
43 | payload: {
44 | id: nanoid(),
45 | title,
46 | content,
47 | date: new Date().toISOString(),
48 | userId,
49 | reactions: {
50 | thumbsUp: 0,
51 | wow: 0,
52 | heart: 0,
53 | rocket: 0,
54 | coffee: 0
55 | }
56 | }
57 | }
58 | }
59 | },
60 | reactionAdded(state, action) {
61 | const { postId, reaction } = action.payload
62 | const existingPost = state.find(post => post.id === postId)
63 | if (existingPost) {
64 | existingPost.reactions[reaction]++
65 | }
66 | }
67 | }
68 | })
69 |
70 | export const selectAllPosts = (state) => state.posts;
71 |
72 | export const { postAdded, reactionAdded } = postsSlice.actions
73 |
74 | export default postsSlice.reducer
--------------------------------------------------------------------------------
/03_lesson_starter/src/features/users/usersSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = [
4 | { id: '0', name: 'Dude Lebowski' },
5 | { id: '1', name: 'Neil Young' },
6 | { id: '2', name: 'Dave Gray' }
7 | ]
8 |
9 | const usersSlice = createSlice({
10 | name: 'users',
11 | initialState,
12 | reducers: {}
13 | })
14 |
15 | export const selectAllUsers = (state) => state.users;
16 |
17 | export default usersSlice.reducer
--------------------------------------------------------------------------------
/03_lesson_starter/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | html {
8 | font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
9 | background: #333;
10 | color: whitesmoke;
11 | }
12 |
13 | body {
14 | min-height: 100vh;
15 | font-size: 1.5rem;
16 | padding: 0 10% 10%;
17 | }
18 |
19 | input,
20 | textarea,
21 | button,
22 | select {
23 | font: inherit;
24 | margin-bottom: 1em;
25 | }
26 |
27 | main {
28 | max-width: 500px;
29 | margin: auto;
30 | }
31 |
32 | section {
33 | margin-top: 1em;
34 | }
35 |
36 | article {
37 | margin: 0.5em 0.5em 0.5em 0;
38 | border: 1px solid whitesmoke;
39 | border-radius: 10px;
40 | padding: 1em;
41 | }
42 |
43 | h1 {
44 | font-size: 3.5rem;
45 | }
46 |
47 | p {
48 | font-family: Arial, Helvetica, sans-serif;
49 | line-height: 1.4;
50 | font-size: 1.2rem;
51 | margin: 0.5em 0;
52 | }
53 |
54 | form {
55 | display: flex;
56 | flex-direction: column;
57 | }
58 |
59 | .postCredit {
60 | font-size: 1rem;
61 | }
62 |
63 | .reactionButton {
64 | margin: 0 0.25em 0 0;
65 | background: transparent;
66 | border: none;
67 | color: whitesmoke;
68 | font-size: 1rem;
69 | }
70 |
--------------------------------------------------------------------------------
/03_lesson_starter/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import { store } from './app/store';
6 | import { Provider } from 'react-redux';
7 |
8 | ReactDOM.render(
9 |
10 |
11 |
12 |
13 | ,
14 | document.getElementById('root')
15 | );
16 |
--------------------------------------------------------------------------------
/04_lesson/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/04_lesson/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "04_lesson",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reduxjs/toolkit": "^1.8.0",
7 | "axios": "^0.26.1",
8 | "date-fns": "^2.28.0",
9 | "react": "^17.0.2",
10 | "react-dom": "^17.0.2",
11 | "react-redux": "^7.2.6",
12 | "react-router-dom": "^6.3.0",
13 | "react-scripts": "5.0.0"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": [
22 | "react-app",
23 | "react-app/jest"
24 | ]
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/04_lesson/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/04_lesson/public/favicon.ico
--------------------------------------------------------------------------------
/04_lesson/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/04_lesson/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/04_lesson/public/logo192.png
--------------------------------------------------------------------------------
/04_lesson/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/04_lesson/public/logo512.png
--------------------------------------------------------------------------------
/04_lesson/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/04_lesson/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/04_lesson/src/App.js:
--------------------------------------------------------------------------------
1 | import PostsList from "./features/posts/PostsList";
2 | import AddPostForm from "./features/posts/AddPostForm";
3 | import SinglePostPage from "./features/posts/SinglePostPage";
4 | import EditPostForm from "./features/posts/EditPostForm";
5 | import Layout from "./components/Layout";
6 | import { Routes, Route } from 'react-router-dom';
7 |
8 | function App() {
9 | return (
10 |
11 | }>
12 |
13 | } />
14 |
15 |
16 | } />
17 | } />
18 | } />
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | export default App;
27 |
--------------------------------------------------------------------------------
/04_lesson/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import postsReducer from '../features/posts/postsSlice';
3 | import usersReducer from '../features/users/usersSlice';
4 |
5 |
6 | export const store = configureStore({
7 | reducer: {
8 | posts: postsReducer,
9 | users: usersReducer
10 | }
11 | })
--------------------------------------------------------------------------------
/04_lesson/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom"
2 |
3 | const Header = () => {
4 | return (
5 |
6 | Redux Blog
7 |
13 |
14 | )
15 | }
16 |
17 | export default Header
--------------------------------------------------------------------------------
/04_lesson/src/components/Layout.js:
--------------------------------------------------------------------------------
1 | import { Outlet } from 'react-router-dom';
2 | import Header from './Header';
3 |
4 | const Layout = () => {
5 | return (
6 | <>
7 |
8 |
9 |
10 |
11 | >
12 | )
13 | }
14 |
15 | export default Layout
--------------------------------------------------------------------------------
/04_lesson/src/features/posts/PostAuthor.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectAllUsers } from "../users/usersSlice";
3 |
4 | const PostAuthor = ({ userId }) => {
5 | const users = useSelector(selectAllUsers)
6 |
7 | const author = users.find(user => user.id === userId);
8 |
9 | return by {author ? author.name : 'Unknown author'}
10 | }
11 | export default PostAuthor
--------------------------------------------------------------------------------
/04_lesson/src/features/posts/PostsExcerpt.js:
--------------------------------------------------------------------------------
1 | import PostAuthor from "./PostAuthor";
2 | import TimeAgo from "./TimeAgo";
3 | import ReactionButtons from "./ReactionButtons";
4 | import { Link } from 'react-router-dom';
5 |
6 | const PostsExcerpt = ({ post }) => {
7 | return (
8 |
9 | {post.title}
10 | {post.body.substring(0, 75)}...
11 |
12 | View Post
13 |
14 |
15 |
16 |
17 |
18 | )
19 | }
20 | export default PostsExcerpt
--------------------------------------------------------------------------------
/04_lesson/src/features/posts/PostsList.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectAllPosts, getPostsStatus, getPostsError } from "./postsSlice";
3 | import PostsExcerpt from "./PostsExcerpt";
4 |
5 | const PostsList = () => {
6 |
7 | const posts = useSelector(selectAllPosts);
8 | const postStatus = useSelector(getPostsStatus);
9 | const error = useSelector(getPostsError);
10 |
11 | let content;
12 | if (postStatus === 'loading') {
13 | content = "Loading..."
;
14 | } else if (postStatus === 'succeeded') {
15 | const orderedPosts = posts.slice().sort((a, b) => b.date.localeCompare(a.date))
16 | content = orderedPosts.map(post => )
17 | } else if (postStatus === 'failed') {
18 | content = {error}
;
19 | }
20 |
21 | return (
22 |
25 | )
26 | }
27 | export default PostsList
--------------------------------------------------------------------------------
/04_lesson/src/features/posts/ReactionButtons.js:
--------------------------------------------------------------------------------
1 | import { useDispatch } from "react-redux";
2 | import { reactionAdded } from "./postsSlice";
3 |
4 | const reactionEmoji = {
5 | thumbsUp: '👍',
6 | wow: '😮',
7 | heart: '❤️',
8 | rocket: '🚀',
9 | coffee: '☕'
10 | }
11 |
12 | const ReactionButtons = ({ post }) => {
13 | const dispatch = useDispatch()
14 |
15 | const reactionButtons = Object.entries(reactionEmoji).map(([name, emoji]) => {
16 | return (
17 |
27 | )
28 | })
29 |
30 | return {reactionButtons}
31 | }
32 | export default ReactionButtons
--------------------------------------------------------------------------------
/04_lesson/src/features/posts/SinglePostPage.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux'
2 | import { selectPostById } from './postsSlice'
3 |
4 | import PostAuthor from "./PostAuthor";
5 | import TimeAgo from "./TimeAgo";
6 | import ReactionButtons from "./ReactionButtons";
7 |
8 | import { useParams } from 'react-router-dom';
9 | import { Link } from 'react-router-dom';
10 |
11 | const SinglePostPage = () => {
12 | const { postId } = useParams()
13 |
14 | const post = useSelector((state) => selectPostById(state, Number(postId)))
15 |
16 | if (!post) {
17 | return (
18 |
19 | Post not found!
20 |
21 | )
22 | }
23 |
24 | return (
25 |
26 | {post.title}
27 | {post.body}
28 |
29 | Edit Post
30 |
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | export default SinglePostPage
--------------------------------------------------------------------------------
/04_lesson/src/features/posts/TimeAgo.js:
--------------------------------------------------------------------------------
1 | import { parseISO, formatDistanceToNow } from 'date-fns';
2 |
3 | const TimeAgo = ({ timestamp }) => {
4 | let timeAgo = ''
5 | if (timestamp) {
6 | const date = parseISO(timestamp)
7 | const timePeriod = formatDistanceToNow(date)
8 | timeAgo = `${timePeriod} ago`
9 | }
10 |
11 | return (
12 |
13 | {timeAgo}
14 |
15 | )
16 | }
17 | export default TimeAgo
--------------------------------------------------------------------------------
/04_lesson/src/features/users/usersSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
2 | import axios from "axios";
3 |
4 | const USERS_URL = 'https://jsonplaceholder.typicode.com/users';
5 |
6 | const initialState = []
7 |
8 | export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
9 | const response = await axios.get(USERS_URL);
10 | return response.data
11 | })
12 |
13 | const usersSlice = createSlice({
14 | name: 'users',
15 | initialState,
16 | reducers: {},
17 | extraReducers(builder) {
18 | builder.addCase(fetchUsers.fulfilled, (state, action) => {
19 | return action.payload;
20 | })
21 | }
22 | })
23 |
24 | export const selectAllUsers = (state) => state.users;
25 |
26 | export default usersSlice.reducer
--------------------------------------------------------------------------------
/04_lesson/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | html {
8 | font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
9 | background-color: white;
10 | color: #000;
11 | }
12 |
13 | body {
14 | min-height: 100vh;
15 | font-size: 1.5rem;
16 | }
17 |
18 | input,
19 | textarea,
20 | button,
21 | select {
22 | font: inherit;
23 | margin-bottom: 1em;
24 | }
25 |
26 | header {
27 | padding: 1rem;
28 | display: flex;
29 | justify-content: space-between;
30 | align-items: flex-start;
31 | background-color: purple;
32 | color: whitesmoke;
33 | position: sticky;
34 | top: 0;
35 | }
36 |
37 | nav {
38 | display: flex;
39 | justify-content: flex-end;
40 | }
41 |
42 | nav ul {
43 | list-style-type: none;
44 | }
45 |
46 | nav ul li {
47 | display: inline-block;
48 | margin-right: 1rem;
49 | }
50 |
51 | nav a, nav a:visited {
52 | color: #fff;
53 | text-decoration: none;
54 | }
55 |
56 | nav a:hover, nav a:focus {
57 | text-decoration: underline;
58 | }
59 |
60 | main {
61 | max-width: 500px;
62 | margin: auto;
63 | }
64 |
65 | section {
66 | margin-top: 1em;
67 | }
68 |
69 | article {
70 | margin: 0.5em;
71 | border: 1px solid #000;
72 | border-radius: 10px;
73 | padding: 1em;
74 | }
75 |
76 | h1 {
77 | font-size: 3.5rem;
78 | }
79 |
80 | h2 {
81 | margin-bottom: 1rem;
82 | }
83 |
84 | p {
85 | font-family: Arial, Helvetica, sans-serif;
86 | line-height: 1.4;
87 | font-size: 1.2rem;
88 | margin: 0.5em 0;
89 | }
90 |
91 | form {
92 | display: flex;
93 | flex-direction: column;
94 | }
95 |
96 | textarea {
97 | height: 200px;
98 | }
99 |
100 | .postCredit {
101 | font-size: 1rem;
102 | }
103 |
104 | .postCredit a,
105 | .postCredit a:visited {
106 | margin-right: 0.5rem;
107 | color: black;
108 | }
109 |
110 | .postCredit a:hover,
111 | .postCredit a:focus {
112 | color: hsla(0, 0%, 0%, 0.75);
113 | }
114 |
115 | .excerpt {
116 | font-style: italic;
117 | }
118 |
119 | .reactionButton {
120 | margin: 0 0.25em 0 0;
121 | background: transparent;
122 | border: none;
123 | color: #000;
124 | font-size: 1rem;
125 | }
126 |
127 | .deleteButton {
128 | background-color: palevioletred;
129 | color: white;
130 | }
--------------------------------------------------------------------------------
/04_lesson/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import { store } from './app/store';
6 | import { Provider } from 'react-redux';
7 | import { fetchPosts } from './features/posts/postsSlice';
8 | import { fetchUsers } from './features/users/usersSlice';
9 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
10 |
11 | store.dispatch(fetchPosts());
12 | store.dispatch(fetchUsers());
13 |
14 | ReactDOM.render(
15 |
16 |
17 |
18 |
19 | } />
20 |
21 |
22 |
23 | ,
24 | document.getElementById('root')
25 | );
26 |
--------------------------------------------------------------------------------
/04_lesson_starter/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/04_lesson_starter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "04_lesson",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reduxjs/toolkit": "^1.8.0",
7 | "axios": "^0.26.1",
8 | "date-fns": "^2.28.0",
9 | "react": "^17.0.2",
10 | "react-dom": "^17.0.2",
11 | "react-redux": "^7.2.6",
12 | "react-scripts": "5.0.0"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "eject": "react-scripts eject"
18 | },
19 | "eslintConfig": {
20 | "extends": [
21 | "react-app",
22 | "react-app/jest"
23 | ]
24 | },
25 | "browserslist": {
26 | "production": [
27 | ">0.2%",
28 | "not dead",
29 | "not op_mini all"
30 | ],
31 | "development": [
32 | "last 1 chrome version",
33 | "last 1 firefox version",
34 | "last 1 safari version"
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/04_lesson_starter/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/04_lesson_starter/public/favicon.ico
--------------------------------------------------------------------------------
/04_lesson_starter/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/04_lesson_starter/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/04_lesson_starter/public/logo192.png
--------------------------------------------------------------------------------
/04_lesson_starter/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/04_lesson_starter/public/logo512.png
--------------------------------------------------------------------------------
/04_lesson_starter/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/04_lesson_starter/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/04_lesson_starter/src/App.js:
--------------------------------------------------------------------------------
1 | import PostsList from "./features/posts/PostsList";
2 | import AddPostForm from "./features/posts/AddPostForm";
3 |
4 | function App() {
5 | return (
6 |
7 |
8 |
9 |
10 | );
11 | }
12 |
13 | export default App;
14 |
--------------------------------------------------------------------------------
/04_lesson_starter/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import postsReducer from '../features/posts/postsSlice';
3 | import usersReducer from '../features/users/usersSlice';
4 |
5 |
6 | export const store = configureStore({
7 | reducer: {
8 | posts: postsReducer,
9 | users: usersReducer
10 | }
11 | })
--------------------------------------------------------------------------------
/04_lesson_starter/src/features/posts/PostAuthor.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectAllUsers } from "../users/usersSlice";
3 |
4 | const PostAuthor = ({ userId }) => {
5 | const users = useSelector(selectAllUsers)
6 |
7 | const author = users.find(user => user.id === userId);
8 |
9 | return by {author ? author.name : 'Unknown author'}
10 | }
11 | export default PostAuthor
--------------------------------------------------------------------------------
/04_lesson_starter/src/features/posts/PostsExcerpt.js:
--------------------------------------------------------------------------------
1 | import PostAuthor from "./PostAuthor";
2 | import TimeAgo from "./TimeAgo";
3 | import ReactionButtons from "./ReactionButtons";
4 |
5 | const PostsExcerpt = ({ post }) => {
6 | return (
7 |
8 | {post.title}
9 | {post.body.substring(0, 100)}
10 |
11 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 | export default PostsExcerpt
--------------------------------------------------------------------------------
/04_lesson_starter/src/features/posts/PostsList.js:
--------------------------------------------------------------------------------
1 | import { useSelector, useDispatch } from "react-redux";
2 | import { selectAllPosts, getPostsStatus, getPostsError, fetchPosts } from "./postsSlice";
3 | import { useEffect } from "react";
4 | import PostsExcerpt from "./PostsExcerpt";
5 |
6 | const PostsList = () => {
7 | const dispatch = useDispatch();
8 |
9 | const posts = useSelector(selectAllPosts);
10 | const postStatus = useSelector(getPostsStatus);
11 | const error = useSelector(getPostsError);
12 |
13 | useEffect(() => {
14 | if (postStatus === 'idle') {
15 | dispatch(fetchPosts())
16 | }
17 | }, [postStatus, dispatch])
18 |
19 | let content;
20 | if (postStatus === 'loading') {
21 | content = "Loading..."
;
22 | } else if (postStatus === 'succeeded') {
23 | const orderedPosts = posts.slice().sort((a, b) => b.date.localeCompare(a.date))
24 | content = orderedPosts.map(post => )
25 | } else if (postStatus === 'failed') {
26 | content = {error}
;
27 | }
28 |
29 | return (
30 |
31 | Posts
32 | {content}
33 |
34 | )
35 | }
36 | export default PostsList
--------------------------------------------------------------------------------
/04_lesson_starter/src/features/posts/ReactionButtons.js:
--------------------------------------------------------------------------------
1 | import { useDispatch } from "react-redux";
2 | import { reactionAdded } from "./postsSlice";
3 |
4 | const reactionEmoji = {
5 | thumbsUp: '👍',
6 | wow: '😮',
7 | heart: '❤️',
8 | rocket: '🚀',
9 | coffee: '☕'
10 | }
11 |
12 | const ReactionButtons = ({ post }) => {
13 | const dispatch = useDispatch()
14 |
15 | const reactionButtons = Object.entries(reactionEmoji).map(([name, emoji]) => {
16 | return (
17 |
27 | )
28 | })
29 |
30 | return {reactionButtons}
31 | }
32 | export default ReactionButtons
--------------------------------------------------------------------------------
/04_lesson_starter/src/features/posts/TimeAgo.js:
--------------------------------------------------------------------------------
1 | import { parseISO, formatDistanceToNow } from 'date-fns';
2 |
3 | const TimeAgo = ({ timestamp }) => {
4 | let timeAgo = ''
5 | if (timestamp) {
6 | const date = parseISO(timestamp)
7 | const timePeriod = formatDistanceToNow(date)
8 | timeAgo = `${timePeriod} ago`
9 | }
10 |
11 | return (
12 |
13 | {timeAgo}
14 |
15 | )
16 | }
17 | export default TimeAgo
--------------------------------------------------------------------------------
/04_lesson_starter/src/features/users/usersSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
2 | import axios from "axios";
3 |
4 | const USERS_URL = 'https://jsonplaceholder.typicode.com/users';
5 |
6 | const initialState = []
7 |
8 | export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
9 | const response = await axios.get(USERS_URL);
10 | return response.data
11 | })
12 |
13 | const usersSlice = createSlice({
14 | name: 'users',
15 | initialState,
16 | reducers: {},
17 | extraReducers(builder) {
18 | builder.addCase(fetchUsers.fulfilled, (state, action) => {
19 | return action.payload;
20 | })
21 | }
22 | })
23 |
24 | export const selectAllUsers = (state) => state.users;
25 |
26 | export default usersSlice.reducer
--------------------------------------------------------------------------------
/04_lesson_starter/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | html {
8 | font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
9 | background-color: white;
10 | color: #000;
11 | }
12 |
13 | body {
14 | min-height: 100vh;
15 | font-size: 1.5rem;
16 | }
17 |
18 | input,
19 | textarea,
20 | button,
21 | select {
22 | font: inherit;
23 | margin-bottom: 1em;
24 | }
25 |
26 | header {
27 | padding: 1rem;
28 | display: flex;
29 | justify-content: space-between;
30 | align-items: flex-start;
31 | background-color: purple;
32 | color: whitesmoke;
33 | position: sticky;
34 | top: 0;
35 | }
36 |
37 | nav {
38 | display: flex;
39 | justify-content: flex-end;
40 | }
41 |
42 | nav ul {
43 | list-style-type: none;
44 | }
45 |
46 | nav ul li {
47 | display: inline-block;
48 | margin-right: 1rem;
49 | }
50 |
51 | nav a, nav a:visited {
52 | color: #fff;
53 | text-decoration: none;
54 | }
55 |
56 | nav a:hover, nav a:focus {
57 | text-decoration: underline;
58 | }
59 |
60 | main {
61 | max-width: 500px;
62 | margin: auto;
63 | }
64 |
65 | section {
66 | margin-top: 1em;
67 | }
68 |
69 | article {
70 | margin: 0.5em;
71 | border: 1px solid #000;
72 | border-radius: 10px;
73 | padding: 1em;
74 | }
75 |
76 | h1 {
77 | font-size: 3.5rem;
78 | }
79 |
80 | h2 {
81 | margin-bottom: 1rem;
82 | }
83 |
84 | p {
85 | font-family: Arial, Helvetica, sans-serif;
86 | line-height: 1.4;
87 | font-size: 1.2rem;
88 | margin: 0.5em 0;
89 | }
90 |
91 | form {
92 | display: flex;
93 | flex-direction: column;
94 | }
95 |
96 | textarea {
97 | height: 200px;
98 | }
99 |
100 | .postCredit {
101 | font-size: 1rem;
102 | }
103 |
104 | .postCredit a,
105 | .postCredit a:visited {
106 | margin-right: 0.5rem;
107 | color: black;
108 | }
109 |
110 | .postCredit a:hover,
111 | .postCredit a:focus {
112 | color: hsla(0, 0%, 0%, 0.75);
113 | }
114 |
115 | .excerpt {
116 | font-style: italic;
117 | }
118 |
119 | .reactionButton {
120 | margin: 0 0.25em 0 0;
121 | background: transparent;
122 | border: none;
123 | color: #000;
124 | font-size: 1rem;
125 | }
126 |
127 | .deleteButton {
128 | background-color: palevioletred;
129 | color: white;
130 | }
--------------------------------------------------------------------------------
/04_lesson_starter/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import { store } from './app/store';
6 | import { Provider } from 'react-redux';
7 | import { fetchUsers } from './features/users/usersSlice';
8 |
9 | store.dispatch(fetchUsers());
10 |
11 | ReactDOM.render(
12 |
13 |
14 |
15 |
16 | ,
17 | document.getElementById('root')
18 | );
19 |
--------------------------------------------------------------------------------
/05_lesson/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/05_lesson/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "05_lesson",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reduxjs/toolkit": "^1.8.0",
7 | "axios": "^0.26.1",
8 | "date-fns": "^2.28.0",
9 | "react": "^17.0.2",
10 | "react-dom": "^17.0.2",
11 | "react-redux": "^7.2.6",
12 | "react-router-dom": "^6.3.0",
13 | "react-scripts": "5.0.0"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": [
22 | "react-app",
23 | "react-app/jest"
24 | ]
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/05_lesson/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/05_lesson/public/favicon.ico
--------------------------------------------------------------------------------
/05_lesson/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/05_lesson/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/05_lesson/public/logo192.png
--------------------------------------------------------------------------------
/05_lesson/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/05_lesson/public/logo512.png
--------------------------------------------------------------------------------
/05_lesson/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/05_lesson/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/05_lesson/src/App.js:
--------------------------------------------------------------------------------
1 | import PostsList from "./features/posts/PostsList";
2 | import AddPostForm from "./features/posts/AddPostForm";
3 | import SinglePostPage from "./features/posts/SinglePostPage";
4 | import EditPostForm from "./features/posts/EditPostForm";
5 | import UsersList from "./features/users/UsersList";
6 | import UserPage from './features/users/UserPage';
7 | import Layout from "./components/Layout";
8 | import { Routes, Route, Navigate } from 'react-router-dom';
9 |
10 | function App() {
11 | return (
12 |
13 | }>
14 |
15 | } />
16 |
17 |
18 | } />
19 | } />
20 | } />
21 |
22 |
23 |
24 | } />
25 | } />
26 |
27 |
28 | {/* Catch all - replace with 404 component if you want */}
29 | } />
30 |
31 |
32 |
33 | );
34 | }
35 |
36 | export default App;
37 |
--------------------------------------------------------------------------------
/05_lesson/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import postsReducer from '../features/posts/postsSlice';
3 | import usersReducer from '../features/users/usersSlice';
4 |
5 |
6 | export const store = configureStore({
7 | reducer: {
8 | posts: postsReducer,
9 | users: usersReducer
10 | }
11 | })
--------------------------------------------------------------------------------
/05_lesson/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom"
2 | import { useDispatch, useSelector } from "react-redux"
3 | import { increaseCount, getCount } from "../features/posts/postsSlice"
4 |
5 | const Header = () => {
6 | const dispatch = useDispatch()
7 | const count = useSelector(getCount)
8 |
9 | return (
10 |
11 | Redux Blog
12 |
24 |
25 | )
26 | }
27 |
28 | export default Header
--------------------------------------------------------------------------------
/05_lesson/src/components/Layout.js:
--------------------------------------------------------------------------------
1 | import { Outlet } from 'react-router-dom';
2 | import Header from './Header';
3 |
4 | const Layout = () => {
5 | return (
6 | <>
7 |
8 |
9 |
10 |
11 | >
12 | )
13 | }
14 |
15 | export default Layout
--------------------------------------------------------------------------------
/05_lesson/src/features/posts/PostAuthor.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectAllUsers } from "../users/usersSlice";
3 |
4 | const PostAuthor = ({ userId }) => {
5 | const users = useSelector(selectAllUsers)
6 |
7 | const author = users.find(user => user.id === userId);
8 |
9 | return by {author ? author.name : 'Unknown author'}
10 | }
11 | export default PostAuthor
--------------------------------------------------------------------------------
/05_lesson/src/features/posts/PostsExcerpt.js:
--------------------------------------------------------------------------------
1 | import PostAuthor from "./PostAuthor";
2 | import TimeAgo from "./TimeAgo";
3 | import ReactionButtons from "./ReactionButtons";
4 | import { Link } from 'react-router-dom';
5 |
6 | import { useSelector } from "react-redux";
7 | import { selectPostById } from "./postsSlice";
8 |
9 | const PostsExcerpt = ({ postId }) => {
10 | const post = useSelector(state => selectPostById(state, postId))
11 |
12 | return (
13 |
14 | {post.title}
15 | {post.body.substring(0, 75)}...
16 |
17 | View Post
18 |
19 |
20 |
21 |
22 |
23 | )
24 | }
25 |
26 | export default PostsExcerpt
--------------------------------------------------------------------------------
/05_lesson/src/features/posts/PostsList.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectPostIds, getPostsStatus, getPostsError } from "./postsSlice";
3 | import PostsExcerpt from "./PostsExcerpt";
4 |
5 | const PostsList = () => {
6 |
7 | const orderedPostIds = useSelector(selectPostIds)
8 | const postStatus = useSelector(getPostsStatus);
9 | const error = useSelector(getPostsError);
10 |
11 | let content;
12 | if (postStatus === 'loading') {
13 | content = "Loading..."
;
14 | } else if (postStatus === 'succeeded') {
15 | content = orderedPostIds.map(postId => )
16 | } else if (postStatus === 'failed') {
17 | content = {error}
;
18 | }
19 |
20 | return (
21 |
24 | )
25 | }
26 | export default PostsList
--------------------------------------------------------------------------------
/05_lesson/src/features/posts/ReactionButtons.js:
--------------------------------------------------------------------------------
1 | import { useDispatch } from "react-redux";
2 | import { reactionAdded } from "./postsSlice";
3 |
4 | const reactionEmoji = {
5 | thumbsUp: '👍',
6 | wow: '😮',
7 | heart: '❤️',
8 | rocket: '🚀',
9 | coffee: '☕'
10 | }
11 |
12 | const ReactionButtons = ({ post }) => {
13 | const dispatch = useDispatch()
14 |
15 | const reactionButtons = Object.entries(reactionEmoji).map(([name, emoji]) => {
16 | return (
17 |
27 | )
28 | })
29 |
30 | return {reactionButtons}
31 | }
32 | export default ReactionButtons
--------------------------------------------------------------------------------
/05_lesson/src/features/posts/SinglePostPage.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux'
2 | import { selectPostById } from './postsSlice'
3 |
4 | import PostAuthor from "./PostAuthor";
5 | import TimeAgo from "./TimeAgo";
6 | import ReactionButtons from "./ReactionButtons";
7 |
8 | import { useParams } from 'react-router-dom';
9 | import { Link } from 'react-router-dom';
10 |
11 | const SinglePostPage = () => {
12 | const { postId } = useParams()
13 |
14 | const post = useSelector((state) => selectPostById(state, Number(postId)))
15 |
16 | if (!post) {
17 | return (
18 |
19 | Post not found!
20 |
21 | )
22 | }
23 |
24 | return (
25 |
26 | {post.title}
27 | {post.body}
28 |
29 | Edit Post
30 |
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | export default SinglePostPage
--------------------------------------------------------------------------------
/05_lesson/src/features/posts/TimeAgo.js:
--------------------------------------------------------------------------------
1 | import { parseISO, formatDistanceToNow } from 'date-fns';
2 |
3 | const TimeAgo = ({ timestamp }) => {
4 | let timeAgo = ''
5 | if (timestamp) {
6 | const date = parseISO(timestamp)
7 | const timePeriod = formatDistanceToNow(date)
8 | timeAgo = `${timePeriod} ago`
9 | }
10 |
11 | return (
12 |
13 | {timeAgo}
14 |
15 | )
16 | }
17 | export default TimeAgo
--------------------------------------------------------------------------------
/05_lesson/src/features/users/UserPage.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux'
2 | import { selectUserById } from '../users/usersSlice'
3 | import { selectAllPosts, selectPostsByUser } from '../posts/postsSlice'
4 | import { Link, useParams } from 'react-router-dom'
5 |
6 | const UserPage = () => {
7 | const { userId } = useParams()
8 | const user = useSelector(state => selectUserById(state, Number(userId)))
9 |
10 | const postsForUser = useSelector(state => selectPostsByUser(state, Number(userId)))
11 |
12 | const postTitles = postsForUser.map(post => (
13 |
14 | {post.title}
15 |
16 | ))
17 |
18 | return (
19 |
20 | {user?.name}
21 |
22 | {postTitles}
23 |
24 | )
25 | }
26 |
27 | export default UserPage
--------------------------------------------------------------------------------
/05_lesson/src/features/users/UsersList.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux'
2 | import { selectAllUsers } from './usersSlice'
3 | import { Link } from 'react-router-dom'
4 |
5 | const UsersList = () => {
6 | const users = useSelector(selectAllUsers)
7 |
8 | const renderedUsers = users.map(user => (
9 |
10 | {user.name}
11 |
12 | ))
13 |
14 | return (
15 |
16 | Users
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default UsersList
--------------------------------------------------------------------------------
/05_lesson/src/features/users/usersSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
2 | import axios from "axios";
3 |
4 | const USERS_URL = 'https://jsonplaceholder.typicode.com/users';
5 |
6 | const initialState = []
7 |
8 | export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
9 | const response = await axios.get(USERS_URL);
10 | return response.data
11 | })
12 |
13 | const usersSlice = createSlice({
14 | name: 'users',
15 | initialState,
16 | reducers: {},
17 | extraReducers(builder) {
18 | builder.addCase(fetchUsers.fulfilled, (state, action) => {
19 | return action.payload;
20 | })
21 | }
22 | })
23 |
24 | export const selectAllUsers = (state) => state.users;
25 |
26 | export const selectUserById = (state, userId) =>
27 | state.users.find(user => user.id === userId)
28 |
29 | export default usersSlice.reducer
--------------------------------------------------------------------------------
/05_lesson/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | html {
8 | font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
9 | background-color: white;
10 | color: #000;
11 | }
12 |
13 | body {
14 | min-height: 100vh;
15 | font-size: 1.5rem;
16 | }
17 |
18 | input,
19 | textarea,
20 | button,
21 | select {
22 | font: inherit;
23 | margin-bottom: 1em;
24 | }
25 |
26 | header {
27 | padding: 1rem;
28 | display: flex;
29 | justify-content: space-between;
30 | align-items: flex-start;
31 | background-color: purple;
32 | color: whitesmoke;
33 | position: sticky;
34 | top: 0;
35 | }
36 |
37 | nav {
38 | display: flex;
39 | justify-content: flex-end;
40 | }
41 |
42 | nav ul {
43 | list-style-type: none;
44 | }
45 |
46 | nav ul li {
47 | display: inline-block;
48 | margin-right: 1rem;
49 | }
50 |
51 | nav a, nav a:visited {
52 | color: #fff;
53 | text-decoration: none;
54 | }
55 |
56 | nav a:hover, nav a:focus {
57 | text-decoration: underline;
58 | }
59 |
60 | main {
61 | max-width: 500px;
62 | margin: auto;
63 | }
64 |
65 | section {
66 | margin-top: 1em;
67 | }
68 |
69 | article {
70 | margin: 0.5em;
71 | border: 1px solid #000;
72 | border-radius: 10px;
73 | padding: 1em;
74 | }
75 |
76 | h1 {
77 | font-size: 3.5rem;
78 | }
79 |
80 | h2 {
81 | margin-bottom: 1rem;
82 | }
83 |
84 | p {
85 | font-family: Arial, Helvetica, sans-serif;
86 | line-height: 1.4;
87 | font-size: 1.2rem;
88 | margin: 0.5em 0;
89 | }
90 |
91 | form {
92 | display: flex;
93 | flex-direction: column;
94 | }
95 |
96 | textarea {
97 | height: 200px;
98 | }
99 |
100 | .postCredit {
101 | font-size: 1rem;
102 | }
103 |
104 | .postCredit a,
105 | .postCredit a:visited {
106 | margin-right: 0.5rem;
107 | color: black;
108 | }
109 |
110 | .postCredit a:hover,
111 | .postCredit a:focus {
112 | color: hsla(0, 0%, 0%, 0.75);
113 | }
114 |
115 | .excerpt {
116 | font-style: italic;
117 | }
118 |
119 | .reactionButton {
120 | margin: 0 0.25em 0 0;
121 | background: transparent;
122 | border: none;
123 | color: #000;
124 | font-size: 1rem;
125 | }
126 |
127 | .deleteButton {
128 | background-color: palevioletred;
129 | color: white;
130 | }
--------------------------------------------------------------------------------
/05_lesson/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import { store } from './app/store';
6 | import { Provider } from 'react-redux';
7 | import { fetchPosts } from './features/posts/postsSlice';
8 | import { fetchUsers } from './features/users/usersSlice';
9 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
10 |
11 | store.dispatch(fetchPosts());
12 | store.dispatch(fetchUsers());
13 |
14 | ReactDOM.render(
15 |
16 |
17 |
18 |
19 | } />
20 |
21 |
22 |
23 | ,
24 | document.getElementById('root')
25 | );
26 |
--------------------------------------------------------------------------------
/05_lesson_starter/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/05_lesson_starter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "05_lesson",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reduxjs/toolkit": "^1.8.0",
7 | "axios": "^0.26.1",
8 | "date-fns": "^2.28.0",
9 | "react": "^17.0.2",
10 | "react-dom": "^17.0.2",
11 | "react-redux": "^7.2.6",
12 | "react-router-dom": "^6.3.0",
13 | "react-scripts": "5.0.0"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": [
22 | "react-app",
23 | "react-app/jest"
24 | ]
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/05_lesson_starter/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/05_lesson_starter/public/favicon.ico
--------------------------------------------------------------------------------
/05_lesson_starter/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/05_lesson_starter/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/05_lesson_starter/public/logo192.png
--------------------------------------------------------------------------------
/05_lesson_starter/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/05_lesson_starter/public/logo512.png
--------------------------------------------------------------------------------
/05_lesson_starter/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/05_lesson_starter/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/05_lesson_starter/src/App.js:
--------------------------------------------------------------------------------
1 | import PostsList from "./features/posts/PostsList";
2 | import AddPostForm from "./features/posts/AddPostForm";
3 | import SinglePostPage from "./features/posts/SinglePostPage";
4 | import EditPostForm from "./features/posts/EditPostForm";
5 | import Layout from "./components/Layout";
6 | import { Routes, Route } from 'react-router-dom';
7 |
8 | function App() {
9 | return (
10 |
11 | }>
12 |
13 | } />
14 |
15 |
16 | } />
17 | } />
18 | } />
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | export default App;
27 |
--------------------------------------------------------------------------------
/05_lesson_starter/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import postsReducer from '../features/posts/postsSlice';
3 | import usersReducer from '../features/users/usersSlice';
4 |
5 |
6 | export const store = configureStore({
7 | reducer: {
8 | posts: postsReducer,
9 | users: usersReducer
10 | }
11 | })
--------------------------------------------------------------------------------
/05_lesson_starter/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom"
2 |
3 | const Header = () => {
4 | return (
5 |
6 | Redux Blog
7 |
13 |
14 | )
15 | }
16 |
17 | export default Header
--------------------------------------------------------------------------------
/05_lesson_starter/src/components/Layout.js:
--------------------------------------------------------------------------------
1 | import { Outlet } from 'react-router-dom';
2 | import Header from './Header';
3 |
4 | const Layout = () => {
5 | return (
6 | <>
7 |
8 |
9 |
10 |
11 | >
12 | )
13 | }
14 |
15 | export default Layout
--------------------------------------------------------------------------------
/05_lesson_starter/src/features/posts/PostAuthor.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectAllUsers } from "../users/usersSlice";
3 |
4 | const PostAuthor = ({ userId }) => {
5 | const users = useSelector(selectAllUsers)
6 |
7 | const author = users.find(user => user.id === userId);
8 |
9 | return by {author ? author.name : 'Unknown author'}
10 | }
11 | export default PostAuthor
--------------------------------------------------------------------------------
/05_lesson_starter/src/features/posts/PostsExcerpt.js:
--------------------------------------------------------------------------------
1 | import PostAuthor from "./PostAuthor";
2 | import TimeAgo from "./TimeAgo";
3 | import ReactionButtons from "./ReactionButtons";
4 | import { Link } from 'react-router-dom';
5 |
6 | const PostsExcerpt = ({ post }) => {
7 | return (
8 |
9 | {post.title}
10 | {post.body.substring(0, 75)}...
11 |
12 | View Post
13 |
14 |
15 |
16 |
17 |
18 | )
19 | }
20 | export default PostsExcerpt
--------------------------------------------------------------------------------
/05_lesson_starter/src/features/posts/PostsList.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectAllPosts, getPostsStatus, getPostsError } from "./postsSlice";
3 | import PostsExcerpt from "./PostsExcerpt";
4 |
5 | const PostsList = () => {
6 |
7 | const posts = useSelector(selectAllPosts);
8 | const postStatus = useSelector(getPostsStatus);
9 | const error = useSelector(getPostsError);
10 |
11 | let content;
12 | if (postStatus === 'loading') {
13 | content = "Loading..."
;
14 | } else if (postStatus === 'succeeded') {
15 | const orderedPosts = posts.slice().sort((a, b) => b.date.localeCompare(a.date))
16 | content = orderedPosts.map(post => )
17 | } else if (postStatus === 'failed') {
18 | content = {error}
;
19 | }
20 |
21 | return (
22 |
25 | )
26 | }
27 | export default PostsList
--------------------------------------------------------------------------------
/05_lesson_starter/src/features/posts/ReactionButtons.js:
--------------------------------------------------------------------------------
1 | import { useDispatch } from "react-redux";
2 | import { reactionAdded } from "./postsSlice";
3 |
4 | const reactionEmoji = {
5 | thumbsUp: '👍',
6 | wow: '😮',
7 | heart: '❤️',
8 | rocket: '🚀',
9 | coffee: '☕'
10 | }
11 |
12 | const ReactionButtons = ({ post }) => {
13 | const dispatch = useDispatch()
14 |
15 | const reactionButtons = Object.entries(reactionEmoji).map(([name, emoji]) => {
16 | return (
17 |
27 | )
28 | })
29 |
30 | return {reactionButtons}
31 | }
32 | export default ReactionButtons
--------------------------------------------------------------------------------
/05_lesson_starter/src/features/posts/SinglePostPage.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux'
2 | import { selectPostById } from './postsSlice'
3 |
4 | import PostAuthor from "./PostAuthor";
5 | import TimeAgo from "./TimeAgo";
6 | import ReactionButtons from "./ReactionButtons";
7 |
8 | import { useParams } from 'react-router-dom';
9 | import { Link } from 'react-router-dom';
10 |
11 | const SinglePostPage = () => {
12 | const { postId } = useParams()
13 |
14 | const post = useSelector((state) => selectPostById(state, Number(postId)))
15 |
16 | if (!post) {
17 | return (
18 |
19 | Post not found!
20 |
21 | )
22 | }
23 |
24 | return (
25 |
26 | {post.title}
27 | {post.body}
28 |
29 | Edit Post
30 |
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | export default SinglePostPage
--------------------------------------------------------------------------------
/05_lesson_starter/src/features/posts/TimeAgo.js:
--------------------------------------------------------------------------------
1 | import { parseISO, formatDistanceToNow } from 'date-fns';
2 |
3 | const TimeAgo = ({ timestamp }) => {
4 | let timeAgo = ''
5 | if (timestamp) {
6 | const date = parseISO(timestamp)
7 | const timePeriod = formatDistanceToNow(date)
8 | timeAgo = `${timePeriod} ago`
9 | }
10 |
11 | return (
12 |
13 | {timeAgo}
14 |
15 | )
16 | }
17 | export default TimeAgo
--------------------------------------------------------------------------------
/05_lesson_starter/src/features/users/usersSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
2 | import axios from "axios";
3 |
4 | const USERS_URL = 'https://jsonplaceholder.typicode.com/users';
5 |
6 | const initialState = []
7 |
8 | export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
9 | const response = await axios.get(USERS_URL);
10 | return response.data
11 | })
12 |
13 | const usersSlice = createSlice({
14 | name: 'users',
15 | initialState,
16 | reducers: {},
17 | extraReducers(builder) {
18 | builder.addCase(fetchUsers.fulfilled, (state, action) => {
19 | return action.payload;
20 | })
21 | }
22 | })
23 |
24 | export const selectAllUsers = (state) => state.users;
25 |
26 | export default usersSlice.reducer
--------------------------------------------------------------------------------
/05_lesson_starter/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | html {
8 | font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
9 | background-color: white;
10 | color: #000;
11 | }
12 |
13 | body {
14 | min-height: 100vh;
15 | font-size: 1.5rem;
16 | }
17 |
18 | input,
19 | textarea,
20 | button,
21 | select {
22 | font: inherit;
23 | margin-bottom: 1em;
24 | }
25 |
26 | header {
27 | padding: 1rem;
28 | display: flex;
29 | justify-content: space-between;
30 | align-items: flex-start;
31 | background-color: purple;
32 | color: whitesmoke;
33 | position: sticky;
34 | top: 0;
35 | }
36 |
37 | nav {
38 | display: flex;
39 | justify-content: flex-end;
40 | }
41 |
42 | nav ul {
43 | list-style-type: none;
44 | }
45 |
46 | nav ul li {
47 | display: inline-block;
48 | margin-right: 1rem;
49 | }
50 |
51 | nav a, nav a:visited {
52 | color: #fff;
53 | text-decoration: none;
54 | }
55 |
56 | nav a:hover, nav a:focus {
57 | text-decoration: underline;
58 | }
59 |
60 | main {
61 | max-width: 500px;
62 | margin: auto;
63 | }
64 |
65 | section {
66 | margin-top: 1em;
67 | }
68 |
69 | article {
70 | margin: 0.5em;
71 | border: 1px solid #000;
72 | border-radius: 10px;
73 | padding: 1em;
74 | }
75 |
76 | h1 {
77 | font-size: 3.5rem;
78 | }
79 |
80 | h2 {
81 | margin-bottom: 1rem;
82 | }
83 |
84 | p {
85 | font-family: Arial, Helvetica, sans-serif;
86 | line-height: 1.4;
87 | font-size: 1.2rem;
88 | margin: 0.5em 0;
89 | }
90 |
91 | form {
92 | display: flex;
93 | flex-direction: column;
94 | }
95 |
96 | textarea {
97 | height: 200px;
98 | }
99 |
100 | .postCredit {
101 | font-size: 1rem;
102 | }
103 |
104 | .postCredit a,
105 | .postCredit a:visited {
106 | margin-right: 0.5rem;
107 | color: black;
108 | }
109 |
110 | .postCredit a:hover,
111 | .postCredit a:focus {
112 | color: hsla(0, 0%, 0%, 0.75);
113 | }
114 |
115 | .excerpt {
116 | font-style: italic;
117 | }
118 |
119 | .reactionButton {
120 | margin: 0 0.25em 0 0;
121 | background: transparent;
122 | border: none;
123 | color: #000;
124 | font-size: 1rem;
125 | }
126 |
127 | .deleteButton {
128 | background-color: palevioletred;
129 | color: white;
130 | }
--------------------------------------------------------------------------------
/05_lesson_starter/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import { store } from './app/store';
6 | import { Provider } from 'react-redux';
7 | import { fetchPosts } from './features/posts/postsSlice';
8 | import { fetchUsers } from './features/users/usersSlice';
9 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
10 |
11 | store.dispatch(fetchPosts());
12 | store.dispatch(fetchUsers());
13 |
14 | ReactDOM.render(
15 |
16 |
17 |
18 |
19 | } />
20 |
21 |
22 |
23 | ,
24 | document.getElementById('root')
25 | );
26 |
--------------------------------------------------------------------------------
/06_lesson/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/06_lesson/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rtk-query-intro",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@fortawesome/fontawesome-svg-core": "^6.1.1",
7 | "@fortawesome/free-solid-svg-icons": "^6.1.1",
8 | "@fortawesome/react-fontawesome": "^0.1.18",
9 | "@reduxjs/toolkit": "^1.8.1",
10 | "react": "^18.0.0",
11 | "react-dom": "^18.0.0",
12 | "react-redux": "^7.2.8",
13 | "react-scripts": "5.0.1"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
20 | },
21 | "eslintConfig": {
22 | "extends": "react-app"
23 | },
24 | "browserslist": {
25 | "production": [
26 | ">0.2%",
27 | "not dead",
28 | "not op_mini all"
29 | ],
30 | "development": [
31 | "last 1 chrome version",
32 | "last 1 firefox version",
33 | "last 1 safari version"
34 | ]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/06_lesson/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/06_lesson/public/favicon.ico
--------------------------------------------------------------------------------
/06_lesson/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React Redux App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/06_lesson/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/06_lesson/public/logo192.png
--------------------------------------------------------------------------------
/06_lesson/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/06_lesson/public/logo512.png
--------------------------------------------------------------------------------
/06_lesson/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/06_lesson/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/06_lesson/src/App.js:
--------------------------------------------------------------------------------
1 | import TodoList from "./features/todos/TodoList";
2 |
3 | function App() {
4 | return
5 | }
6 |
7 | export default App;
8 |
--------------------------------------------------------------------------------
/06_lesson/src/features/api/apiSlice.js:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
2 |
3 | export const apiSlice = createApi({
4 | reducerPath: 'api',
5 | baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:3500' }),
6 | tagTypes: ['Todos'],
7 | endpoints: (builder) => ({
8 | getTodos: builder.query({
9 | query: () => '/todos',
10 | transformResponse: res => res.sort((a, b) => b.id - a.id),
11 | providesTags: ['Todos']
12 | }),
13 | addTodo: builder.mutation({
14 | query: (todo) => ({
15 | url: '/todos',
16 | method: 'POST',
17 | body: todo
18 | }),
19 | invalidatesTags: ['Todos']
20 | }),
21 | updateTodo: builder.mutation({
22 | query: (todo) => ({
23 | url: `/todos/${todo.id}`,
24 | method: 'PATCH',
25 | body: todo
26 | }),
27 | invalidatesTags: ['Todos']
28 | }),
29 | deleteTodo: builder.mutation({
30 | query: ({ id }) => ({
31 | url: `/todos/${id}`,
32 | method: 'DELETE',
33 | body: id
34 | }),
35 | invalidatesTags: ['Todos']
36 | }),
37 | })
38 | })
39 |
40 | export const {
41 | useGetTodosQuery,
42 | useAddTodoMutation,
43 | useUpdateTodoMutation,
44 | useDeleteTodoMutation
45 | } = apiSlice
46 |
--------------------------------------------------------------------------------
/06_lesson/src/features/todos/TodoList.js:
--------------------------------------------------------------------------------
1 | import {
2 | useGetTodosQuery,
3 | useUpdateTodoMutation,
4 | useDeleteTodoMutation,
5 | useAddTodoMutation
6 | } from "../api/apiSlice"
7 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
8 | import { faTrash, faUpload } from '@fortawesome/free-solid-svg-icons'
9 | import { useState } from "react"
10 |
11 | const TodoList = () => {
12 | const [newTodo, setNewTodo] = useState('')
13 |
14 | const {
15 | data: todos,
16 | isLoading,
17 | isSuccess,
18 | isError,
19 | error
20 | } = useGetTodosQuery()
21 | const [addTodo] = useAddTodoMutation()
22 | const [updateTodo] = useUpdateTodoMutation()
23 | const [deleteTodo] = useDeleteTodoMutation()
24 |
25 | const handleSubmit = (e) => {
26 | e.preventDefault();
27 | addTodo({ userId: 1, title: newTodo, completed: false })
28 | setNewTodo('')
29 | }
30 |
31 | const newItemSection =
32 |
47 |
48 |
49 | let content;
50 | if (isLoading) {
51 | content = Loading...
52 | } else if (isSuccess) {
53 | content = todos.map(todo => { //JSON.stringify(todos)
54 | return (
55 |
56 |
57 | updateTodo({ ...todo, completed: !todo.completed })}
62 | />
63 |
64 |
65 |
68 |
69 | )
70 | })
71 | } else if (isError) {
72 | content = {error}
73 | }
74 |
75 | return (
76 |
77 | Todo List
78 | {newItemSection}
79 | {content}
80 |
81 | )
82 | }
83 | export default TodoList
--------------------------------------------------------------------------------
/06_lesson/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Nunito&display=swap');
2 |
3 | body {
4 | font-family: 'Nunito', sans-serif;
5 | font-size: 1.5rem;
6 | }
7 |
8 | input[type="text"], button {
9 | font: inherit;
10 | }
11 |
12 | main {
13 | margin: auto;
14 | max-width: 600px;
15 | }
16 |
17 | h1 {
18 | margin-bottom: 0.5rem;
19 | }
20 |
21 | article {
22 | padding: 1rem;
23 | display: flex;
24 | justify-content: space-between;
25 | align-items: center;
26 | border: 1px solid hsl(0, 0%, 58%);
27 | }
28 |
29 | .todo {
30 | display: flex;
31 | justify-content: flex-start;
32 | align-items: center;
33 | }
34 |
35 | input[type="checkbox"] {
36 | min-width: 30px;
37 | min-height: 30px;
38 | margin-right: 1rem;
39 | }
40 |
41 | button {
42 | min-width: 50px;
43 | min-height: 50px;
44 | border: 1px solid #333;
45 | border-radius: 10%;
46 | cursor: pointer;
47 | }
48 |
49 | .trash {
50 | background-color: #fff;
51 | color: mediumvioletred;
52 | }
53 |
54 | .trash:focus, .trash:hover {
55 | filter:brightness(120%)
56 | }
57 |
58 | form {
59 | padding: 1rem;
60 | display: flex;
61 | justify-content: space-between;
62 | align-items: center;
63 | border: 1px solid #333;
64 | margin-bottom: 1rem;
65 | }
66 |
67 | form label {
68 | position: absolute;
69 | left: -10000px;
70 | }
71 |
72 | .new-todo {
73 | width: 100%;
74 | padding-right: 30px;
75 | }
76 |
77 | input[type="text"] {
78 | width: 100%;
79 | padding: 0.5rem;
80 | border-radius: 10px;
81 | border: 0.5px solid #333;
82 | }
83 |
84 | .submit {
85 | background-color: gray;
86 | color: #fff;
87 | }
88 |
89 | .submit:focus, .submit:hover {
90 | background-color: limegreen;
91 | }
--------------------------------------------------------------------------------
/06_lesson/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 |
6 | import { ApiProvider } from "@reduxjs/toolkit/query/react";
7 | import { apiSlice } from "./features/api/apiSlice";
8 |
9 | ReactDOM.createRoot(document.getElementById('root'))
10 | .render(
11 |
12 |
13 |
14 |
15 |
16 | );
--------------------------------------------------------------------------------
/06_lesson_starter/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/06_lesson_starter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rtk-query-intro",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@fortawesome/fontawesome-svg-core": "^6.1.1",
7 | "@fortawesome/free-solid-svg-icons": "^6.1.1",
8 | "@fortawesome/react-fontawesome": "^0.1.18",
9 | "@reduxjs/toolkit": "^1.8.1",
10 | "react": "^18.0.0",
11 | "react-dom": "^18.0.0",
12 | "react-redux": "^7.2.8",
13 | "react-scripts": "5.0.1"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
20 | },
21 | "eslintConfig": {
22 | "extends": "react-app"
23 | },
24 | "browserslist": {
25 | "production": [
26 | ">0.2%",
27 | "not dead",
28 | "not op_mini all"
29 | ],
30 | "development": [
31 | "last 1 chrome version",
32 | "last 1 firefox version",
33 | "last 1 safari version"
34 | ]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/06_lesson_starter/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/06_lesson_starter/public/favicon.ico
--------------------------------------------------------------------------------
/06_lesson_starter/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React Redux App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/06_lesson_starter/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/06_lesson_starter/public/logo192.png
--------------------------------------------------------------------------------
/06_lesson_starter/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/06_lesson_starter/public/logo512.png
--------------------------------------------------------------------------------
/06_lesson_starter/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/06_lesson_starter/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/06_lesson_starter/src/App.js:
--------------------------------------------------------------------------------
1 | import TodoList from "./features/todos/TodoList";
2 |
3 | function App() {
4 | return
5 | }
6 |
7 | export default App;
8 |
--------------------------------------------------------------------------------
/06_lesson_starter/src/features/todos/TodoList.js:
--------------------------------------------------------------------------------
1 | // add imports
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3 | import { faTrash, faUpload } from '@fortawesome/free-solid-svg-icons'
4 | import { useState } from "react"
5 |
6 | const TodoList = () => {
7 | const [newTodo, setNewTodo] = useState('')
8 |
9 | const handleSubmit = (e) => {
10 | e.preventDefault();
11 | //addTodo
12 | setNewTodo('')
13 | }
14 |
15 | const newItemSection =
16 |
31 |
32 |
33 | let content;
34 | // Define conditional content
35 |
36 | return (
37 |
38 | Todo List
39 | {newItemSection}
40 | {content}
41 |
42 | )
43 | }
44 | export default TodoList
--------------------------------------------------------------------------------
/06_lesson_starter/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Nunito&display=swap');
2 |
3 | body {
4 | font-family: 'Nunito', sans-serif;
5 | font-size: 1.5rem;
6 | }
7 |
8 | input[type="text"], button {
9 | font: inherit;
10 | }
11 |
12 | main {
13 | margin: auto;
14 | max-width: 600px;
15 | }
16 |
17 | h1 {
18 | margin-bottom: 0.5rem;
19 | }
20 |
21 | article {
22 | padding: 1rem;
23 | display: flex;
24 | justify-content: space-between;
25 | align-items: center;
26 | border: 1px solid hsl(0, 0%, 58%);
27 | }
28 |
29 | .todo {
30 | display: flex;
31 | justify-content: flex-start;
32 | align-items: center;
33 | }
34 |
35 | input[type="checkbox"] {
36 | min-width: 30px;
37 | min-height: 30px;
38 | margin-right: 1rem;
39 | }
40 |
41 | button {
42 | min-width: 50px;
43 | min-height: 50px;
44 | border: 1px solid #333;
45 | border-radius: 10%;
46 | cursor: pointer;
47 | }
48 |
49 | .trash {
50 | background-color: #fff;
51 | color: mediumvioletred;
52 | }
53 |
54 | .trash:focus, .trash:hover {
55 | filter:brightness(120%)
56 | }
57 |
58 | form {
59 | padding: 1rem;
60 | display: flex;
61 | justify-content: space-between;
62 | align-items: center;
63 | border: 1px solid #333;
64 | margin-bottom: 1rem;
65 | }
66 |
67 | form label {
68 | position: absolute;
69 | left: -10000px;
70 | }
71 |
72 | .new-todo {
73 | width: 100%;
74 | padding-right: 30px;
75 | }
76 |
77 | input[type="text"] {
78 | width: 100%;
79 | padding: 0.5rem;
80 | border-radius: 10px;
81 | border: 0.5px solid #333;
82 | }
83 |
84 | .submit {
85 | background-color: gray;
86 | color: #fff;
87 | }
88 |
89 | .submit:focus, .submit:hover {
90 | background-color: limegreen;
91 | }
--------------------------------------------------------------------------------
/06_lesson_starter/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 |
6 | ReactDOM.createRoot(document.getElementById('root'))
7 | .render(
8 |
9 |
10 |
11 | );
--------------------------------------------------------------------------------
/07_lesson/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/07_lesson/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "07_lesson",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reduxjs/toolkit": "^1.8.0",
7 | "date-fns": "^2.28.0",
8 | "react": "^17.0.2",
9 | "react-dom": "^17.0.2",
10 | "react-redux": "^7.2.6",
11 | "react-router-dom": "^6.3.0",
12 | "react-scripts": "5.0.0"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "eject": "react-scripts eject"
18 | },
19 | "eslintConfig": {
20 | "extends": [
21 | "react-app",
22 | "react-app/jest"
23 | ]
24 | },
25 | "browserslist": {
26 | "production": [
27 | ">0.2%",
28 | "not dead",
29 | "not op_mini all"
30 | ],
31 | "development": [
32 | "last 1 chrome version",
33 | "last 1 firefox version",
34 | "last 1 safari version"
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/07_lesson/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/07_lesson/public/favicon.ico
--------------------------------------------------------------------------------
/07_lesson/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/07_lesson/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/07_lesson/public/logo192.png
--------------------------------------------------------------------------------
/07_lesson/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/07_lesson/public/logo512.png
--------------------------------------------------------------------------------
/07_lesson/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/07_lesson/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/07_lesson/src/App.js:
--------------------------------------------------------------------------------
1 | import PostsList from "./features/posts/PostsList";
2 | import AddPostForm from "./features/posts/AddPostForm";
3 | import SinglePostPage from "./features/posts/SinglePostPage";
4 | import EditPostForm from "./features/posts/EditPostForm";
5 | import UsersList from "./features/users/UsersList";
6 | import UserPage from './features/users/UserPage';
7 | import Layout from "./components/Layout";
8 | import { Routes, Route, Navigate } from 'react-router-dom';
9 |
10 | function App() {
11 | return (
12 |
13 | }>
14 |
15 | } />
16 |
17 |
18 | } />
19 | } />
20 | } />
21 |
22 |
23 |
24 | } />
25 | } />
26 |
27 |
28 | {/* Catch all - replace with 404 component if you want */}
29 | } />
30 |
31 |
32 |
33 | );
34 | }
35 |
36 | export default App;
37 |
--------------------------------------------------------------------------------
/07_lesson/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import { apiSlice } from '../features/api/apiSlice';
3 |
4 | export const store = configureStore({
5 | reducer: {
6 | [apiSlice.reducerPath]: apiSlice.reducer
7 | },
8 | middleware: getDefaultMiddleware =>
9 | getDefaultMiddleware().concat(apiSlice.middleware),
10 | devTools: true
11 | })
--------------------------------------------------------------------------------
/07_lesson/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom"
2 |
3 | const Header = () => {
4 |
5 | return (
6 |
7 | Redux Blog
8 |
15 |
16 | )
17 | }
18 |
19 | export default Header
--------------------------------------------------------------------------------
/07_lesson/src/components/Layout.js:
--------------------------------------------------------------------------------
1 | import { Outlet } from 'react-router-dom';
2 | import Header from './Header';
3 |
4 | const Layout = () => {
5 | return (
6 | <>
7 |
8 |
9 |
10 |
11 | >
12 | )
13 | }
14 |
15 | export default Layout
--------------------------------------------------------------------------------
/07_lesson/src/features/api/apiSlice.js:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
2 |
3 | export const apiSlice = createApi({
4 | reducerPath: 'api', // optional
5 | baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:3500' }),
6 | tagTypes: ['Post', 'User'],
7 | endpoints: builder => ({})
8 | })
--------------------------------------------------------------------------------
/07_lesson/src/features/posts/AddPostForm.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useSelector } from "react-redux";
3 |
4 | import { selectAllUsers } from "../users/usersSlice";
5 | import { useNavigate } from "react-router-dom";
6 | import { useAddNewPostMutation } from "./postsSlice";
7 |
8 | const AddPostForm = () => {
9 | const [addNewPost, { isLoading }] = useAddNewPostMutation()
10 |
11 | const navigate = useNavigate()
12 |
13 | const [title, setTitle] = useState('')
14 | const [content, setContent] = useState('')
15 | const [userId, setUserId] = useState('')
16 |
17 | const users = useSelector(selectAllUsers)
18 |
19 | const onTitleChanged = e => setTitle(e.target.value)
20 | const onContentChanged = e => setContent(e.target.value)
21 | const onAuthorChanged = e => setUserId(e.target.value)
22 |
23 |
24 | const canSave = [title, content, userId].every(Boolean) && !isLoading;
25 |
26 | const onSavePostClicked = async () => {
27 | if (canSave) {
28 | try {
29 | await addNewPost({ title, body: content, userId }).unwrap()
30 |
31 | setTitle('')
32 | setContent('')
33 | setUserId('')
34 | navigate('/')
35 | } catch (err) {
36 | console.error('Failed to save the post', err)
37 | }
38 | }
39 | }
40 |
41 | const usersOptions = users.map(user => (
42 |
45 | ))
46 |
47 | return (
48 |
78 | )
79 | }
80 | export default AddPostForm
--------------------------------------------------------------------------------
/07_lesson/src/features/posts/PostAuthor.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectAllUsers } from "../users/usersSlice";
3 | import { Link } from "react-router-dom";
4 |
5 | const PostAuthor = ({ userId }) => {
6 | const users = useSelector(selectAllUsers)
7 |
8 | const author = users.find(user => user.id === userId);
9 |
10 | return by {author
11 | ? {author.name}
12 | : 'Unknown author'}
13 | }
14 | export default PostAuthor
--------------------------------------------------------------------------------
/07_lesson/src/features/posts/PostsExcerpt.js:
--------------------------------------------------------------------------------
1 | import PostAuthor from "./PostAuthor";
2 | import TimeAgo from "./TimeAgo";
3 | import ReactionButtons from "./ReactionButtons";
4 | import { Link } from 'react-router-dom';
5 |
6 | import { useSelector } from "react-redux";
7 | import { selectPostById } from "./postsSlice";
8 |
9 | const PostsExcerpt = ({ postId }) => {
10 | const post = useSelector(state => selectPostById(state, postId))
11 |
12 | return (
13 |
14 | {post.title}
15 | {post.body.substring(0, 75)}...
16 |
17 | View Post
18 |
19 |
20 |
21 |
22 |
23 | )
24 | }
25 |
26 | export default PostsExcerpt
--------------------------------------------------------------------------------
/07_lesson/src/features/posts/PostsList.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectPostIds } from "./postsSlice";
3 | import PostsExcerpt from "./PostsExcerpt";
4 | import { useGetPostsQuery } from './postsSlice';
5 |
6 | const PostsList = () => {
7 | const {
8 | isLoading,
9 | isSuccess,
10 | isError,
11 | error
12 | } = useGetPostsQuery()
13 |
14 | const orderedPostIds = useSelector(selectPostIds)
15 |
16 | let content;
17 | if (isLoading) {
18 | content = "Loading..."
;
19 | } else if (isSuccess) {
20 | content = orderedPostIds.map(postId => )
21 | } else if (isError) {
22 | content = {error}
;
23 | }
24 |
25 | return (
26 |
29 | )
30 | }
31 | export default PostsList
--------------------------------------------------------------------------------
/07_lesson/src/features/posts/ReactionButtons.js:
--------------------------------------------------------------------------------
1 | import { useAddReactionMutation } from './postsSlice'
2 |
3 | const reactionEmoji = {
4 | thumbsUp: '👍',
5 | wow: '😮',
6 | heart: '❤️',
7 | rocket: '🚀',
8 | coffee: '☕'
9 | }
10 |
11 | const ReactionButtons = ({ post }) => {
12 | const [addReaction] = useAddReactionMutation()
13 |
14 | const reactionButtons = Object.entries(reactionEmoji).map(([name, emoji]) => {
15 | return (
16 |
27 | )
28 | })
29 |
30 | return {reactionButtons}
31 | }
32 | export default ReactionButtons
--------------------------------------------------------------------------------
/07_lesson/src/features/posts/SinglePostPage.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux'
2 | import { selectPostById } from './postsSlice'
3 |
4 | import PostAuthor from "./PostAuthor";
5 | import TimeAgo from "./TimeAgo";
6 | import ReactionButtons from "./ReactionButtons";
7 |
8 | import { useParams } from 'react-router-dom';
9 | import { Link } from 'react-router-dom';
10 |
11 | const SinglePostPage = () => {
12 | const { postId } = useParams()
13 |
14 | const post = useSelector((state) => selectPostById(state, Number(postId)))
15 |
16 | if (!post) {
17 | return (
18 |
19 | Post not found!
20 |
21 | )
22 | }
23 |
24 | return (
25 |
26 | {post.title}
27 | {post.body}
28 |
29 | Edit Post
30 |
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | export default SinglePostPage
--------------------------------------------------------------------------------
/07_lesson/src/features/posts/TimeAgo.js:
--------------------------------------------------------------------------------
1 | import { parseISO, formatDistanceToNow } from 'date-fns';
2 |
3 | const TimeAgo = ({ timestamp }) => {
4 | let timeAgo = ''
5 | if (timestamp) {
6 | const date = parseISO(timestamp)
7 | const timePeriod = formatDistanceToNow(date)
8 | timeAgo = `${timePeriod} ago`
9 | }
10 |
11 | return (
12 |
13 | {timeAgo}
14 |
15 | )
16 | }
17 | export default TimeAgo
--------------------------------------------------------------------------------
/07_lesson/src/features/users/UserPage.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux'
2 | import { selectUserById } from '../users/usersSlice'
3 | import { Link, useParams } from 'react-router-dom'
4 | import { useGetPostsByUserIdQuery } from '../posts/postsSlice'
5 |
6 | const UserPage = () => {
7 | const { userId } = useParams()
8 | const user = useSelector(state => selectUserById(state, Number(userId)))
9 |
10 | const {
11 | data: postsForUser,
12 | isLoading,
13 | isSuccess,
14 | isError,
15 | error
16 | } = useGetPostsByUserIdQuery(userId);
17 |
18 | let content;
19 | if (isLoading) {
20 | content = Loading...
21 | } else if (isSuccess) {
22 | const { ids, entities } = postsForUser
23 | content = ids.map(id => (
24 |
25 | {entities[id].title}
26 |
27 | ))
28 | } else if (isError) {
29 | content = {error}
;
30 | }
31 |
32 | return (
33 |
34 | {user?.name}
35 |
36 | {content}
37 |
38 | )
39 | }
40 |
41 | export default UserPage
--------------------------------------------------------------------------------
/07_lesson/src/features/users/UsersList.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux'
2 | import { selectAllUsers } from './usersSlice'
3 | import { Link } from 'react-router-dom'
4 |
5 | const UsersList = () => {
6 | const users = useSelector(selectAllUsers)
7 |
8 | const renderedUsers = users.map(user => (
9 |
10 | {user.name}
11 |
12 | ))
13 |
14 | return (
15 |
16 | Users
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default UsersList
--------------------------------------------------------------------------------
/07_lesson/src/features/users/usersSlice.js:
--------------------------------------------------------------------------------
1 | import {
2 | createSelector,
3 | createEntityAdapter
4 | } from "@reduxjs/toolkit";
5 | import { apiSlice } from "../api/apiSlice";
6 |
7 | const usersAdapter = createEntityAdapter()
8 |
9 | const initialState = usersAdapter.getInitialState()
10 |
11 | export const usersApiSlice = apiSlice.injectEndpoints({
12 | endpoints: builder => ({
13 | getUsers: builder.query({
14 | query: () => '/users',
15 | transformResponse: responseData => {
16 | return usersAdapter.setAll(initialState, responseData)
17 | },
18 | providesTags: (result, error, arg) => [
19 | { type: 'User', id: "LIST" },
20 | ...result.ids.map(id => ({ type: 'User', id }))
21 | ]
22 | })
23 | })
24 | })
25 |
26 | export const {
27 | useGetUsersQuery
28 | } = usersApiSlice
29 |
30 | // returns the query result object
31 | export const selectUsersResult = usersApiSlice.endpoints.getUsers.select()
32 |
33 | // Creates memoized selector
34 | const selectUsersData = createSelector(
35 | selectUsersResult,
36 | usersResult => usersResult.data // normalized state object with ids & entities
37 | )
38 |
39 | //getSelectors creates these selectors and we rename them with aliases using destructuring
40 | export const {
41 | selectAll: selectAllUsers,
42 | selectById: selectUserById,
43 | selectIds: selectUserIds
44 | // Pass in a selector that returns the posts slice of state
45 | } = usersAdapter.getSelectors(state => selectUsersData(state) ?? initialState)
46 |
--------------------------------------------------------------------------------
/07_lesson/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | html {
8 | font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
9 | background-color: white;
10 | color: #000;
11 | }
12 |
13 | body {
14 | min-height: 100vh;
15 | font-size: 1.5rem;
16 | }
17 |
18 | input,
19 | textarea,
20 | button,
21 | select {
22 | font: inherit;
23 | margin-bottom: 1em;
24 | }
25 |
26 | header {
27 | padding: 1rem;
28 | display: flex;
29 | justify-content: space-between;
30 | align-items: flex-start;
31 | background-color: purple;
32 | color: whitesmoke;
33 | position: sticky;
34 | top: 0;
35 | }
36 |
37 | nav {
38 | display: flex;
39 | justify-content: flex-end;
40 | }
41 |
42 | nav ul {
43 | list-style-type: none;
44 | }
45 |
46 | nav ul li {
47 | display: inline-block;
48 | margin-right: 1rem;
49 | }
50 |
51 | nav a, nav a:visited {
52 | color: #fff;
53 | text-decoration: none;
54 | }
55 |
56 | nav a:hover, nav a:focus {
57 | text-decoration: underline;
58 | }
59 |
60 | main {
61 | max-width: 500px;
62 | margin: auto;
63 | }
64 |
65 | section {
66 | margin-top: 1em;
67 | }
68 |
69 | article {
70 | margin: 0.5em;
71 | border: 1px solid #000;
72 | border-radius: 10px;
73 | padding: 1em;
74 | }
75 |
76 | h1 {
77 | font-size: 3.5rem;
78 | }
79 |
80 | h2 {
81 | margin-bottom: 1rem;
82 | }
83 |
84 | p {
85 | font-family: Arial, Helvetica, sans-serif;
86 | line-height: 1.4;
87 | font-size: 1.2rem;
88 | margin: 0.5em 0;
89 | }
90 |
91 | form {
92 | display: flex;
93 | flex-direction: column;
94 | }
95 |
96 | textarea {
97 | height: 200px;
98 | }
99 |
100 | .postCredit {
101 | font-size: 1rem;
102 | }
103 |
104 | .postCredit a,
105 | .postCredit a:visited {
106 | margin-right: 0.5rem;
107 | color: black;
108 | }
109 |
110 | .postCredit a:hover,
111 | .postCredit a:focus {
112 | color: hsla(0, 0%, 0%, 0.75);
113 | }
114 |
115 | .excerpt {
116 | font-style: italic;
117 | }
118 |
119 | .reactionButton {
120 | margin: 0 0.25em 0 0;
121 | background: transparent;
122 | border: none;
123 | color: #000;
124 | font-size: 1rem;
125 | }
126 |
127 | .deleteButton {
128 | background-color: palevioletred;
129 | color: white;
130 | }
--------------------------------------------------------------------------------
/07_lesson/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import { store } from './app/store';
6 | import { Provider } from 'react-redux';
7 | import { extendedApiSlice } from './features/posts/postsSlice';
8 | import { usersApiSlice } from './features/users/usersSlice';
9 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
10 |
11 | store.dispatch(extendedApiSlice.endpoints.getPosts.initiate());
12 | store.dispatch(usersApiSlice.endpoints.getUsers.initiate());
13 |
14 | ReactDOM.render(
15 |
16 |
17 |
18 |
19 | } />
20 |
21 |
22 |
23 | ,
24 | document.getElementById('root')
25 | );
26 |
--------------------------------------------------------------------------------
/07_lesson_starter/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/07_lesson_starter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "07_lesson",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reduxjs/toolkit": "^1.8.0",
7 | "axios": "^0.26.1",
8 | "date-fns": "^2.28.0",
9 | "react": "^17.0.2",
10 | "react-dom": "^17.0.2",
11 | "react-redux": "^7.2.6",
12 | "react-router-dom": "^6.3.0",
13 | "react-scripts": "5.0.0"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": [
22 | "react-app",
23 | "react-app/jest"
24 | ]
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/07_lesson_starter/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/07_lesson_starter/public/favicon.ico
--------------------------------------------------------------------------------
/07_lesson_starter/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/07_lesson_starter/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/07_lesson_starter/public/logo192.png
--------------------------------------------------------------------------------
/07_lesson_starter/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/07_lesson_starter/public/logo512.png
--------------------------------------------------------------------------------
/07_lesson_starter/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/07_lesson_starter/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/07_lesson_starter/src/App.js:
--------------------------------------------------------------------------------
1 | import PostsList from "./features/posts/PostsList";
2 | import AddPostForm from "./features/posts/AddPostForm";
3 | import SinglePostPage from "./features/posts/SinglePostPage";
4 | import EditPostForm from "./features/posts/EditPostForm";
5 | import UsersList from "./features/users/UsersList";
6 | import UserPage from './features/users/UserPage';
7 | import Layout from "./components/Layout";
8 | import { Routes, Route, Navigate } from 'react-router-dom';
9 |
10 | function App() {
11 | return (
12 |
13 | }>
14 |
15 | } />
16 |
17 |
18 | } />
19 | } />
20 | } />
21 |
22 |
23 |
24 | } />
25 | } />
26 |
27 |
28 | {/* Catch all - replace with 404 component if you want */}
29 | } />
30 |
31 |
32 |
33 | );
34 | }
35 |
36 | export default App;
37 |
--------------------------------------------------------------------------------
/07_lesson_starter/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import postsReducer from '../features/posts/postsSlice';
3 | import usersReducer from '../features/users/usersSlice';
4 |
5 |
6 | export const store = configureStore({
7 | reducer: {
8 | posts: postsReducer,
9 | users: usersReducer
10 | }
11 | })
--------------------------------------------------------------------------------
/07_lesson_starter/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom"
2 | import { useDispatch, useSelector } from "react-redux"
3 | import { increaseCount, getCount } from "../features/posts/postsSlice"
4 |
5 | const Header = () => {
6 | const dispatch = useDispatch()
7 | const count = useSelector(getCount)
8 |
9 | return (
10 |
11 | Redux Blog
12 |
24 |
25 | )
26 | }
27 |
28 | export default Header
--------------------------------------------------------------------------------
/07_lesson_starter/src/components/Layout.js:
--------------------------------------------------------------------------------
1 | import { Outlet } from 'react-router-dom';
2 | import Header from './Header';
3 |
4 | const Layout = () => {
5 | return (
6 | <>
7 |
8 |
9 |
10 |
11 | >
12 | )
13 | }
14 |
15 | export default Layout
--------------------------------------------------------------------------------
/07_lesson_starter/src/features/posts/PostAuthor.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectAllUsers } from "../users/usersSlice";
3 |
4 | const PostAuthor = ({ userId }) => {
5 | const users = useSelector(selectAllUsers)
6 |
7 | const author = users.find(user => user.id === userId);
8 |
9 | return by {author ? author.name : 'Unknown author'}
10 | }
11 | export default PostAuthor
--------------------------------------------------------------------------------
/07_lesson_starter/src/features/posts/PostsExcerpt.js:
--------------------------------------------------------------------------------
1 | import PostAuthor from "./PostAuthor";
2 | import TimeAgo from "./TimeAgo";
3 | import ReactionButtons from "./ReactionButtons";
4 | import { Link } from 'react-router-dom';
5 |
6 | import { useSelector } from "react-redux";
7 | import { selectPostById } from "./postsSlice";
8 |
9 | const PostsExcerpt = ({ postId }) => {
10 | const post = useSelector(state => selectPostById(state, postId))
11 |
12 | return (
13 |
14 | {post.title}
15 | {post.body.substring(0, 75)}...
16 |
17 | View Post
18 |
19 |
20 |
21 |
22 |
23 | )
24 | }
25 |
26 | export default PostsExcerpt
--------------------------------------------------------------------------------
/07_lesson_starter/src/features/posts/PostsList.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectPostIds, getPostsStatus, getPostsError } from "./postsSlice";
3 | import PostsExcerpt from "./PostsExcerpt";
4 |
5 | const PostsList = () => {
6 |
7 | const orderedPostIds = useSelector(selectPostIds)
8 | const postStatus = useSelector(getPostsStatus);
9 | const error = useSelector(getPostsError);
10 |
11 | let content;
12 | if (postStatus === 'loading') {
13 | content = "Loading..."
;
14 | } else if (postStatus === 'succeeded') {
15 | content = orderedPostIds.map(postId => )
16 | } else if (postStatus === 'failed') {
17 | content = {error}
;
18 | }
19 |
20 | return (
21 |
24 | )
25 | }
26 | export default PostsList
--------------------------------------------------------------------------------
/07_lesson_starter/src/features/posts/ReactionButtons.js:
--------------------------------------------------------------------------------
1 | import { useDispatch } from "react-redux";
2 | import { reactionAdded } from "./postsSlice";
3 |
4 | const reactionEmoji = {
5 | thumbsUp: '👍',
6 | wow: '😮',
7 | heart: '❤️',
8 | rocket: '🚀',
9 | coffee: '☕'
10 | }
11 |
12 | const ReactionButtons = ({ post }) => {
13 | const dispatch = useDispatch()
14 |
15 | const reactionButtons = Object.entries(reactionEmoji).map(([name, emoji]) => {
16 | return (
17 |
27 | )
28 | })
29 |
30 | return {reactionButtons}
31 | }
32 | export default ReactionButtons
--------------------------------------------------------------------------------
/07_lesson_starter/src/features/posts/SinglePostPage.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux'
2 | import { selectPostById } from './postsSlice'
3 |
4 | import PostAuthor from "./PostAuthor";
5 | import TimeAgo from "./TimeAgo";
6 | import ReactionButtons from "./ReactionButtons";
7 |
8 | import { useParams } from 'react-router-dom';
9 | import { Link } from 'react-router-dom';
10 |
11 | const SinglePostPage = () => {
12 | const { postId } = useParams()
13 |
14 | const post = useSelector((state) => selectPostById(state, Number(postId)))
15 |
16 | if (!post) {
17 | return (
18 |
19 | Post not found!
20 |
21 | )
22 | }
23 |
24 | return (
25 |
26 | {post.title}
27 | {post.body}
28 |
29 | Edit Post
30 |
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | export default SinglePostPage
--------------------------------------------------------------------------------
/07_lesson_starter/src/features/posts/TimeAgo.js:
--------------------------------------------------------------------------------
1 | import { parseISO, formatDistanceToNow } from 'date-fns';
2 |
3 | const TimeAgo = ({ timestamp }) => {
4 | let timeAgo = ''
5 | if (timestamp) {
6 | const date = parseISO(timestamp)
7 | const timePeriod = formatDistanceToNow(date)
8 | timeAgo = `${timePeriod} ago`
9 | }
10 |
11 | return (
12 |
13 | {timeAgo}
14 |
15 | )
16 | }
17 | export default TimeAgo
--------------------------------------------------------------------------------
/07_lesson_starter/src/features/users/UserPage.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux'
2 | import { selectUserById } from '../users/usersSlice'
3 | import { selectAllPosts, selectPostsByUser } from '../posts/postsSlice'
4 | import { Link, useParams } from 'react-router-dom'
5 |
6 | const UserPage = () => {
7 | const { userId } = useParams()
8 | const user = useSelector(state => selectUserById(state, Number(userId)))
9 |
10 | const postsForUser = useSelector(state => selectPostsByUser(state, Number(userId)))
11 |
12 | const postTitles = postsForUser.map(post => (
13 |
14 | {post.title}
15 |
16 | ))
17 |
18 | return (
19 |
20 | {user?.name}
21 |
22 | {postTitles}
23 |
24 | )
25 | }
26 |
27 | export default UserPage
--------------------------------------------------------------------------------
/07_lesson_starter/src/features/users/UsersList.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux'
2 | import { selectAllUsers } from './usersSlice'
3 | import { Link } from 'react-router-dom'
4 |
5 | const UsersList = () => {
6 | const users = useSelector(selectAllUsers)
7 |
8 | const renderedUsers = users.map(user => (
9 |
10 | {user.name}
11 |
12 | ))
13 |
14 | return (
15 |
16 | Users
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default UsersList
--------------------------------------------------------------------------------
/07_lesson_starter/src/features/users/usersSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
2 | import axios from "axios";
3 |
4 | const USERS_URL = 'https://jsonplaceholder.typicode.com/users';
5 |
6 | const initialState = []
7 |
8 | export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
9 | const response = await axios.get(USERS_URL);
10 | return response.data
11 | })
12 |
13 | const usersSlice = createSlice({
14 | name: 'users',
15 | initialState,
16 | reducers: {},
17 | extraReducers(builder) {
18 | builder.addCase(fetchUsers.fulfilled, (state, action) => {
19 | return action.payload;
20 | })
21 | }
22 | })
23 |
24 | export const selectAllUsers = (state) => state.users;
25 |
26 | export const selectUserById = (state, userId) =>
27 | state.users.find(user => user.id === userId)
28 |
29 | export default usersSlice.reducer
--------------------------------------------------------------------------------
/07_lesson_starter/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | html {
8 | font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
9 | background-color: white;
10 | color: #000;
11 | }
12 |
13 | body {
14 | min-height: 100vh;
15 | font-size: 1.5rem;
16 | }
17 |
18 | input,
19 | textarea,
20 | button,
21 | select {
22 | font: inherit;
23 | margin-bottom: 1em;
24 | }
25 |
26 | header {
27 | padding: 1rem;
28 | display: flex;
29 | justify-content: space-between;
30 | align-items: flex-start;
31 | background-color: purple;
32 | color: whitesmoke;
33 | position: sticky;
34 | top: 0;
35 | }
36 |
37 | nav {
38 | display: flex;
39 | justify-content: flex-end;
40 | }
41 |
42 | nav ul {
43 | list-style-type: none;
44 | }
45 |
46 | nav ul li {
47 | display: inline-block;
48 | margin-right: 1rem;
49 | }
50 |
51 | nav a, nav a:visited {
52 | color: #fff;
53 | text-decoration: none;
54 | }
55 |
56 | nav a:hover, nav a:focus {
57 | text-decoration: underline;
58 | }
59 |
60 | main {
61 | max-width: 500px;
62 | margin: auto;
63 | }
64 |
65 | section {
66 | margin-top: 1em;
67 | }
68 |
69 | article {
70 | margin: 0.5em;
71 | border: 1px solid #000;
72 | border-radius: 10px;
73 | padding: 1em;
74 | }
75 |
76 | h1 {
77 | font-size: 3.5rem;
78 | }
79 |
80 | h2 {
81 | margin-bottom: 1rem;
82 | }
83 |
84 | p {
85 | font-family: Arial, Helvetica, sans-serif;
86 | line-height: 1.4;
87 | font-size: 1.2rem;
88 | margin: 0.5em 0;
89 | }
90 |
91 | form {
92 | display: flex;
93 | flex-direction: column;
94 | }
95 |
96 | textarea {
97 | height: 200px;
98 | }
99 |
100 | .postCredit {
101 | font-size: 1rem;
102 | }
103 |
104 | .postCredit a,
105 | .postCredit a:visited {
106 | margin-right: 0.5rem;
107 | color: black;
108 | }
109 |
110 | .postCredit a:hover,
111 | .postCredit a:focus {
112 | color: hsla(0, 0%, 0%, 0.75);
113 | }
114 |
115 | .excerpt {
116 | font-style: italic;
117 | }
118 |
119 | .reactionButton {
120 | margin: 0 0.25em 0 0;
121 | background: transparent;
122 | border: none;
123 | color: #000;
124 | font-size: 1rem;
125 | }
126 |
127 | .deleteButton {
128 | background-color: palevioletred;
129 | color: white;
130 | }
--------------------------------------------------------------------------------
/07_lesson_starter/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import { store } from './app/store';
6 | import { Provider } from 'react-redux';
7 | import { fetchPosts } from './features/posts/postsSlice';
8 | import { fetchUsers } from './features/users/usersSlice';
9 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
10 |
11 | store.dispatch(fetchPosts());
12 | store.dispatch(fetchUsers());
13 |
14 | ReactDOM.render(
15 |
16 |
17 |
18 |
19 | } />
20 |
21 |
22 |
23 | ,
24 | document.getElementById('root')
25 | );
26 |
--------------------------------------------------------------------------------
/08_lesson/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/08_lesson/README.md:
--------------------------------------------------------------------------------
1 | # "React Redux Toolkit for Beginners"
2 |
3 | ### Bonus Lesson - Chapter 8:
4 |
5 | ### Blog Project with full RTK Query Refactor
6 |
7 | ---
8 |
9 | 🚩 This lesson is not included in the Youtube course video or playlist.
10 |
11 | 🚀 Chapter 8 completes the modification of the Blog Project from the course to a full-integration of RTK Query:
12 | - ✅ Relies on RTK Query hooks with identifiable cache keys
13 | - ✅ Removes all instances of the Redux useSelector in favor of RTK useQuery hooks
14 | - ✅ Demonstrates how to use multiple queries, loading states, etc. inside of one component.
15 | - ✅ Demonstrates the use of selectFromResult in useQuery hooks with accompanying loading, success, and error states.
--------------------------------------------------------------------------------
/08_lesson/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "07_lesson",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reduxjs/toolkit": "^1.8.0",
7 | "date-fns": "^2.28.0",
8 | "react": "^17.0.2",
9 | "react-dom": "^17.0.2",
10 | "react-redux": "^7.2.6",
11 | "react-router-dom": "^6.3.0",
12 | "react-scripts": "5.0.0"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "eject": "react-scripts eject"
18 | },
19 | "eslintConfig": {
20 | "extends": [
21 | "react-app",
22 | "react-app/jest"
23 | ]
24 | },
25 | "browserslist": {
26 | "production": [
27 | ">0.2%",
28 | "not dead",
29 | "not op_mini all"
30 | ],
31 | "development": [
32 | "last 1 chrome version",
33 | "last 1 firefox version",
34 | "last 1 safari version"
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/08_lesson/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/08_lesson/public/favicon.ico
--------------------------------------------------------------------------------
/08_lesson/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/08_lesson/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/08_lesson/public/logo192.png
--------------------------------------------------------------------------------
/08_lesson/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_redux_toolkit/573491db427848fc611eafa3048990775214d4ad/08_lesson/public/logo512.png
--------------------------------------------------------------------------------
/08_lesson/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/08_lesson/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/08_lesson/src/App.js:
--------------------------------------------------------------------------------
1 | import PostsList from "./features/posts/PostsList";
2 | import AddPostForm from "./features/posts/AddPostForm";
3 | import SinglePostPage from "./features/posts/SinglePostPage";
4 | import EditPostForm from "./features/posts/EditPostForm";
5 | import UsersList from "./features/users/UsersList";
6 | import UserPage from './features/users/UserPage';
7 | import Layout from "./components/Layout";
8 | import { Routes, Route, Navigate } from 'react-router-dom';
9 |
10 | function App() {
11 | return (
12 |
13 | }>
14 |
15 | } />
16 |
17 |
18 | } />
19 | } />
20 | } />
21 |
22 |
23 |
24 | } />
25 | } />
26 |
27 |
28 | {/* Catch all - replace with 404 component if you want */}
29 | } />
30 |
31 |
32 |
33 | );
34 | }
35 |
36 | export default App;
37 |
--------------------------------------------------------------------------------
/08_lesson/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import { apiSlice } from '../features/api/apiSlice';
3 |
4 | export const store = configureStore({
5 | reducer: {
6 | [apiSlice.reducerPath]: apiSlice.reducer
7 | },
8 | middleware: getDefaultMiddleware =>
9 | getDefaultMiddleware().concat(apiSlice.middleware),
10 | devTools: true
11 | })
--------------------------------------------------------------------------------
/08_lesson/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom"
2 |
3 | const Header = () => {
4 |
5 | return (
6 |
7 | Redux Blog
8 |
15 |
16 | )
17 | }
18 |
19 | export default Header
--------------------------------------------------------------------------------
/08_lesson/src/components/Layout.js:
--------------------------------------------------------------------------------
1 | import { Outlet } from 'react-router-dom';
2 | import Header from './Header';
3 |
4 | const Layout = () => {
5 | return (
6 | <>
7 |
8 |
9 |
10 |
11 | >
12 | )
13 | }
14 |
15 | export default Layout
--------------------------------------------------------------------------------
/08_lesson/src/features/api/apiSlice.js:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
2 |
3 | export const apiSlice = createApi({
4 | reducerPath: 'api', // optional
5 | baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:3500' }),
6 | tagTypes: ['Post', 'User'],
7 | endpoints: builder => ({})
8 | })
--------------------------------------------------------------------------------
/08_lesson/src/features/posts/PostAuthor.js:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import { useGetUsersQuery } from "../users/usersSlice";
3 |
4 | const PostAuthor = ({ userId }) => {
5 |
6 | const { user: author } = useGetUsersQuery('getUsers', {
7 | selectFromResult: ({ data, isLoading }) => ({
8 | user: data?.entities[userId]
9 | }),
10 | })
11 |
12 | return by {author
13 | ? {author.name}
14 | : 'Unknown author'}
15 | }
16 | export default PostAuthor
--------------------------------------------------------------------------------
/08_lesson/src/features/posts/PostsExcerpt.js:
--------------------------------------------------------------------------------
1 | import PostAuthor from "./PostAuthor";
2 | import TimeAgo from "./TimeAgo";
3 | import ReactionButtons from "./ReactionButtons";
4 | import { Link } from 'react-router-dom';
5 | import { useGetPostsQuery } from './postsSlice';
6 |
7 | const PostsExcerpt = ({ postId }) => {
8 |
9 | const { post } = useGetPostsQuery('getPosts', {
10 | selectFromResult: ({ data }) => ({
11 | post: data?.entities[postId]
12 | }),
13 | })
14 |
15 | return (
16 |
17 | {post.title}
18 | {post.body.substring(0, 75)}...
19 |
20 | View Post
21 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | export default PostsExcerpt
--------------------------------------------------------------------------------
/08_lesson/src/features/posts/PostsList.js:
--------------------------------------------------------------------------------
1 | import PostsExcerpt from "./PostsExcerpt";
2 | import { useGetPostsQuery } from './postsSlice';
3 |
4 | const PostsList = () => {
5 | const {
6 | data: posts,
7 | isLoading,
8 | isSuccess,
9 | isError,
10 | error
11 | } = useGetPostsQuery('getPosts')
12 |
13 | let content;
14 | if (isLoading) {
15 | content = "Loading..."
;
16 | } else if (isSuccess) {
17 | content = posts.ids.map(postId => )
18 | } else if (isError) {
19 | content = {error}
;
20 | }
21 |
22 | return (
23 |
26 | )
27 | }
28 | export default PostsList
--------------------------------------------------------------------------------
/08_lesson/src/features/posts/ReactionButtons.js:
--------------------------------------------------------------------------------
1 | import { useAddReactionMutation } from './postsSlice'
2 |
3 | const reactionEmoji = {
4 | thumbsUp: '👍',
5 | wow: '😮',
6 | heart: '❤️',
7 | rocket: '🚀',
8 | coffee: '☕'
9 | }
10 |
11 | const ReactionButtons = ({ post }) => {
12 | const [addReaction] = useAddReactionMutation()
13 |
14 | const reactionButtons = Object.entries(reactionEmoji).map(([name, emoji]) => {
15 | return (
16 |
27 | )
28 | })
29 |
30 | return {reactionButtons}
31 | }
32 | export default ReactionButtons
--------------------------------------------------------------------------------
/08_lesson/src/features/posts/SinglePostPage.js:
--------------------------------------------------------------------------------
1 | import PostAuthor from "./PostAuthor";
2 | import TimeAgo from "./TimeAgo";
3 | import ReactionButtons from "./ReactionButtons";
4 | import { useParams } from 'react-router-dom';
5 | import { Link } from 'react-router-dom';
6 | import { useGetPostsQuery } from "./postsSlice";
7 |
8 | const SinglePostPage = () => {
9 | const { postId } = useParams()
10 |
11 | const { post, isLoading } = useGetPostsQuery('getPosts', {
12 | selectFromResult: ({ data, isLoading }) => ({
13 | post: data?.entities[postId],
14 | isLoading
15 | }),
16 | })
17 |
18 | if (isLoading) return Loading...
19 |
20 | if (!post) {
21 | return (
22 |
23 | Post not found!
24 |
25 | )
26 | }
27 |
28 | return (
29 |
30 | {post.title}
31 | {post.body}
32 |
33 | Edit Post
34 |
35 |
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | export default SinglePostPage
--------------------------------------------------------------------------------
/08_lesson/src/features/posts/TimeAgo.js:
--------------------------------------------------------------------------------
1 | import { parseISO, formatDistanceToNow } from 'date-fns';
2 |
3 | const TimeAgo = ({ timestamp }) => {
4 | let timeAgo = ''
5 | if (timestamp) {
6 | const date = parseISO(timestamp)
7 | const timePeriod = formatDistanceToNow(date)
8 | timeAgo = `${timePeriod} ago`
9 | }
10 |
11 | return (
12 |
13 | {timeAgo}
14 |
15 | )
16 | }
17 | export default TimeAgo
--------------------------------------------------------------------------------
/08_lesson/src/features/users/UserPage.js:
--------------------------------------------------------------------------------
1 | import { Link, useParams } from 'react-router-dom'
2 | import { useGetPostsByUserIdQuery } from '../posts/postsSlice'
3 | import { useGetUsersQuery } from '../users/usersSlice'
4 |
5 | const UserPage = () => {
6 | const { userId } = useParams()
7 |
8 | const { user,
9 | isLoading: isLoadingUser,
10 | isSuccess: isSuccessUser,
11 | isError: isErrorUser,
12 | error: errorUser
13 | } = useGetUsersQuery('getUsers', {
14 | selectFromResult: ({ data, isLoading, isSuccess, isError, error }) => ({
15 | user: data?.entities[userId],
16 | isLoading,
17 | isSuccess,
18 | isError,
19 | error
20 | }),
21 | })
22 |
23 | const {
24 | data: postsForUser,
25 | isLoading,
26 | isSuccess,
27 | isError,
28 | error
29 | } = useGetPostsByUserIdQuery(userId);
30 |
31 | let content;
32 | if (isLoading || isLoadingUser) {
33 | content = Loading...
34 | } else if (isSuccess && isSuccessUser) {
35 | const { ids, entities } = postsForUser
36 | content = (
37 |
38 | {user?.name}
39 |
40 | {ids.map(id => (
41 | -
42 | {entities[id].title}
43 |
44 | ))}
45 |
46 |
47 | )
48 | } else if (isError || isErrorUser) {
49 | content = {error || errorUser}
;
50 | }
51 |
52 | return content
53 | }
54 |
55 | export default UserPage
--------------------------------------------------------------------------------
/08_lesson/src/features/users/UsersList.js:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom'
2 | import { useGetUsersQuery } from './usersSlice'
3 |
4 | const UsersList = () => {
5 |
6 | const {
7 | data: users,
8 | isLoading,
9 | isSuccess,
10 | isError,
11 | error
12 | } = useGetUsersQuery('getUsers')
13 |
14 | let content;
15 | if (isLoading) {
16 | content = "Loading..."
;
17 | } else if (isSuccess) {
18 |
19 | const renderedUsers = users.ids.map(userId => (
20 |
21 | {users.entities[userId].name}
22 |
23 | ))
24 |
25 | content = (
26 |
27 | Users
28 |
29 |
30 |
31 | )
32 | } else if (isError) {
33 | content = {error}
;
34 | }
35 |
36 | return content
37 | }
38 |
39 | export default UsersList
--------------------------------------------------------------------------------
/08_lesson/src/features/users/usersSlice.js:
--------------------------------------------------------------------------------
1 | import { createEntityAdapter } from "@reduxjs/toolkit";
2 | import { apiSlice } from "../api/apiSlice";
3 |
4 | const usersAdapter = createEntityAdapter()
5 |
6 | const initialState = usersAdapter.getInitialState()
7 |
8 | export const usersApiSlice = apiSlice.injectEndpoints({
9 | endpoints: builder => ({
10 | getUsers: builder.query({
11 | query: () => '/users',
12 | transformResponse: responseData => {
13 | return usersAdapter.setAll(initialState, responseData)
14 | },
15 | providesTags: (result, error, arg) => [
16 | { type: 'User', id: "LIST" },
17 | ...result.ids.map(id => ({ type: 'User', id }))
18 | ]
19 | })
20 | })
21 | })
22 |
23 | export const {
24 | useGetUsersQuery
25 | } = usersApiSlice
--------------------------------------------------------------------------------
/08_lesson/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | html {
8 | font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
9 | background-color: white;
10 | color: #000;
11 | }
12 |
13 | body {
14 | min-height: 100vh;
15 | font-size: 1.5rem;
16 | }
17 |
18 | input,
19 | textarea,
20 | button,
21 | select {
22 | font: inherit;
23 | margin-bottom: 1em;
24 | }
25 |
26 | header {
27 | padding: 1rem;
28 | display: flex;
29 | justify-content: space-between;
30 | align-items: flex-start;
31 | background-color: purple;
32 | color: whitesmoke;
33 | position: sticky;
34 | top: 0;
35 | }
36 |
37 | nav {
38 | display: flex;
39 | justify-content: flex-end;
40 | }
41 |
42 | nav ul {
43 | list-style-type: none;
44 | }
45 |
46 | nav ul li {
47 | display: inline-block;
48 | margin-right: 1rem;
49 | }
50 |
51 | nav a, nav a:visited {
52 | color: #fff;
53 | text-decoration: none;
54 | }
55 |
56 | nav a:hover, nav a:focus {
57 | text-decoration: underline;
58 | }
59 |
60 | main {
61 | max-width: 500px;
62 | margin: auto;
63 | }
64 |
65 | section {
66 | margin-top: 1em;
67 | }
68 |
69 | article {
70 | margin: 0.5em;
71 | border: 1px solid #000;
72 | border-radius: 10px;
73 | padding: 1em;
74 | }
75 |
76 | h1 {
77 | font-size: 3.5rem;
78 | }
79 |
80 | h2 {
81 | margin-bottom: 1rem;
82 | }
83 |
84 | p {
85 | font-family: Arial, Helvetica, sans-serif;
86 | line-height: 1.4;
87 | font-size: 1.2rem;
88 | margin: 0.5em 0;
89 | }
90 |
91 | form {
92 | display: flex;
93 | flex-direction: column;
94 | }
95 |
96 | textarea {
97 | height: 200px;
98 | }
99 |
100 | .postCredit {
101 | font-size: 1rem;
102 | }
103 |
104 | .postCredit a,
105 | .postCredit a:visited {
106 | margin-right: 0.5rem;
107 | color: black;
108 | }
109 |
110 | .postCredit a:hover,
111 | .postCredit a:focus {
112 | color: hsla(0, 0%, 0%, 0.75);
113 | }
114 |
115 | .excerpt {
116 | font-style: italic;
117 | }
118 |
119 | .reactionButton {
120 | margin: 0 0.25em 0 0;
121 | background: transparent;
122 | border: none;
123 | color: #000;
124 | font-size: 1rem;
125 | }
126 |
127 | .deleteButton {
128 | background-color: palevioletred;
129 | color: white;
130 | }
--------------------------------------------------------------------------------
/08_lesson/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import { store } from './app/store';
6 | import { Provider } from 'react-redux';
7 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
8 |
9 | ReactDOM.render(
10 |
11 |
12 |
13 |
14 | } />
15 |
16 |
17 |
18 | ,
19 | document.getElementById('root')
20 | );
21 |
--------------------------------------------------------------------------------