Added Chapter 2

02_lesson
gitdagray 2022-03-24 10:08:28 -05:00
parent 21174697d0
commit a405f40185
34 changed files with 55212 additions and 2 deletions

23
02_lesson/.gitignore vendored 100644
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

27251
02_lesson/package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
{
"name": "02_lesson",
"version": "0.1.0",
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^1.8.0",
"date-fns": "^2.28.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-redux": "^7.2.6",
"react-scripts": "5.0.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,13 @@
import PostsList from "./features/posts/PostsList";
import AddPostForm from "./features/posts/AddPostForm";
function App() {
return (
<main className="App">
<AddPostForm />
<PostsList />
</main>
);
}
export default App;

View File

@ -0,0 +1,11 @@
import { configureStore } from "@reduxjs/toolkit";
import postsReducer from '../features/posts/postsSlice';
import usersReducer from '../features/users/usersSlice';
export const store = configureStore({
reducer: {
posts: postsReducer,
users: usersReducer
}
})

View File

@ -0,0 +1,71 @@
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { postAdded } from "./postsSlice";
import { selectAllUsers } from "../users/usersSlice";
const AddPostForm = () => {
const dispatch = useDispatch()
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [userId, setUserId] = useState('')
const users = useSelector(selectAllUsers)
const onTitleChanged = e => setTitle(e.target.value)
const onContentChanged = e => setContent(e.target.value)
const onAuthorChanged = e => setUserId(e.target.value)
const onSavePostClicked = () => {
if (title && content) {
dispatch(
postAdded(title, content, userId)
)
setTitle('')
setContent('')
}
}
const canSave = Boolean(title) && Boolean(content) && Boolean(userId)
const usersOptions = users.map(user => (
<option key={user.id} value={user.id}>
{user.name}
</option>
))
return (
<section>
<h2>Add a New Post</h2>
<form>
<label htmlFor="postTitle">Post Title:</label>
<input
type="text"
id="postTitle"
name="postTitle"
value={title}
onChange={onTitleChanged}
/>
<label htmlFor="postAuthor">Author:</label>
<select id="postAuthor" value={userId} onChange={onAuthorChanged}>
<option value=""></option>
{usersOptions}
</select>
<label htmlFor="postContent">Content:</label>
<textarea
id="postContent"
name="postContent"
value={content}
onChange={onContentChanged}
/>
<button
type="button"
onClick={onSavePostClicked}
disabled={!canSave}
>Save Post</button>
</form>
</section>
)
}
export default AddPostForm

View File

@ -0,0 +1,11 @@
import { useSelector } from "react-redux";
import { selectAllUsers } from "../users/usersSlice";
const PostAuthor = ({ userId }) => {
const users = useSelector(selectAllUsers)
const author = users.find(user => user.id === userId);
return <span>by {author ? author.name : 'Unknown author'}</span>
}
export default PostAuthor

View File

@ -0,0 +1,31 @@
import { useSelector } from "react-redux";
import { selectAllPosts } from "./postsSlice";
import PostAuthor from "./PostAuthor";
import TimeAgo from "./TimeAgo";
import ReactionButtons from "./ReactionButtons";
const PostsList = () => {
const posts = useSelector(selectAllPosts)
const orderedPosts = posts.slice().sort((a, b) => b.date.localeCompare(a.date))
const renderedPosts = orderedPosts.map(post => (
<article key={post.id}>
<h3>{post.title}</h3>
<p>{post.content.substring(0, 100)}</p>
<p className="postCredit">
<PostAuthor userId={post.userId} />
<TimeAgo timestamp={post.date} />
</p>
<ReactionButtons post={post} />
</article>
))
return (
<section>
<h2>Posts</h2>
{renderedPosts}
</section>
)
}
export default PostsList

View File

@ -0,0 +1,32 @@
import { useDispatch } from "react-redux";
import { reactionAdded } from "./postsSlice";
const reactionEmoji = {
thumbsUp: '👍',
wow: '😮',
heart: '❤️',
rocket: '🚀',
coffee: '☕'
}
const ReactionButtons = ({ post }) => {
const dispatch = useDispatch()
const reactionButtons = Object.entries(reactionEmoji).map(([name, emoji]) => {
return (
<button
key={name}
type="button"
className="reactionButton"
onClick={() =>
dispatch(reactionAdded({ postId: post.id, reaction: name }))
}
>
{emoji} {post.reactions[name]}
</button>
)
})
return <div>{reactionButtons}</div>
}
export default ReactionButtons

View File

@ -0,0 +1,17 @@
import { parseISO, formatDistanceToNow } from 'date-fns';
const TimeAgo = ({ timestamp }) => {
let timeAgo = ''
if (timestamp) {
const date = parseISO(timestamp)
const timePeriod = formatDistanceToNow(date)
timeAgo = `${timePeriod} ago`
}
return (
<span title={timestamp}>
&nbsp; <i>{timeAgo}</i>
</span>
)
}
export default TimeAgo

View File

@ -0,0 +1,74 @@
import { createSlice, nanoid } from "@reduxjs/toolkit";
import { sub } from 'date-fns';
const initialState = [
{
id: '1',
title: 'Learning Redux Toolkit',
content: "I've heard good things.",
date: sub(new Date(), { minutes: 10 }).toISOString(),
reactions: {
thumbsUp: 0,
wow: 0,
heart: 0,
rocket: 0,
coffee: 0
}
},
{
id: '2',
title: 'Slices...',
content: "The more I say slice, the more I want pizza.",
date: sub(new Date(), { minutes: 5 }).toISOString(),
reactions: {
thumbsUp: 0,
wow: 0,
heart: 0,
rocket: 0,
coffee: 0
}
}
]
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
postAdded: {
reducer(state, action) {
state.push(action.payload)
},
prepare(title, content, userId) {
return {
payload: {
id: nanoid(),
title,
content,
date: new Date().toISOString(),
userId,
reactions: {
thumbsUp: 0,
wow: 0,
heart: 0,
rocket: 0,
coffee: 0
}
}
}
}
},
reactionAdded(state, action) {
const { postId, reaction } = action.payload
const existingPost = state.find(post => post.id === postId)
if (existingPost) {
existingPost.reactions[reaction]++
}
}
}
})
export const selectAllPosts = (state) => state.posts;
export const { postAdded, reactionAdded } = postsSlice.actions
export default postsSlice.reducer

View File

@ -0,0 +1,17 @@
import { createSlice } from "@reduxjs/toolkit";
const initialState = [
{ id: '0', name: 'Dude Lebowski' },
{ id: '1', name: 'Neil Young' },
{ id: '2', name: 'Dave Gray' }
]
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {}
})
export const selectAllUsers = (state) => state.users;
export default usersSlice.reducer

View File

@ -0,0 +1,69 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
background: #333;
color: whitesmoke;
}
body {
min-height: 100vh;
font-size: 1.5rem;
padding: 0 10% 10%;
}
input,
textarea,
button,
select {
font: inherit;
margin-bottom: 1em;
}
main {
max-width: 500px;
margin: auto;
}
section {
margin-top: 1em;
}
article {
margin: 0.5em 0.5em 0.5em 0;
border: 1px solid whitesmoke;
border-radius: 10px;
padding: 1em;
}
h1 {
font-size: 3.5rem;
}
p {
font-family: Arial, Helvetica, sans-serif;
line-height: 1.4;
font-size: 1.2rem;
margin: 0.5em 0;
}
form {
display: flex;
flex-direction: column;
}
.postCredit {
font-size: 1rem;
}
.reactionButton {
margin: 0 0.25em 0 0;
background: transparent;
border: none;
color: whitesmoke;
font-size: 1rem;
}

View File

@ -0,0 +1,15 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { store } from './app/store';
import { Provider } from 'react-redux';
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);

23
02_lesson_starter/.gitignore vendored 100644
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

27233
02_lesson_starter/package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
{
"name": "02_lesson",
"version": "0.1.0",
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^1.8.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-redux": "^7.2.6",
"react-scripts": "5.0.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,10 @@
function App() {
return (
<main className="App">
</main>
);
}
export default App;

View File

@ -0,0 +1,8 @@
import { configureStore } from "@reduxjs/toolkit";
export const store = configureStore({
reducer: {
}
})

View File

@ -0,0 +1,69 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
background: #333;
color: whitesmoke;
}
body {
min-height: 100vh;
font-size: 1.5rem;
padding: 0 10% 10%;
}
input,
textarea,
button,
select {
font: inherit;
margin-bottom: 1em;
}
main {
max-width: 500px;
margin: auto;
}
section {
margin-top: 1em;
}
article {
margin: 0.5em 0.5em 0.5em 0;
border: 1px solid whitesmoke;
border-radius: 10px;
padding: 1em;
}
h1 {
font-size: 3.5rem;
}
p {
font-family: Arial, Helvetica, sans-serif;
line-height: 1.4;
font-size: 1.2rem;
margin: 0.5em 0;
}
form {
display: flex;
flex-direction: column;
}
.postCredit {
font-size: 1rem;
}
.reactionButton {
margin: 0 0.25em 0 0;
background: transparent;
border: none;
color: whitesmoke;
font-size: 1rem;
}

View File

@ -0,0 +1,15 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { store } from './app/store';
import { Provider } from 'react-redux';
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);

View File

@ -37,9 +37,11 @@
---
### 📚 References
### 📚 Redux References
- 🔗 [Official Site for Redux Toolkit](https://redux-toolkit.js.org/)
- 🔗 [Redux Devtools](https://github.com/reduxjs/redux-devtools)
- 🔗 [Immer.js](https://immerjs.github.io/immer/)
### ⚙ VS Code Extensions I Use:
@ -53,6 +55,8 @@
### 💻 Source Code
- 🔗 [Chapter 1]()
- 🔗 [Chapter 1](https://github.com/gitdagray/react_redux_toolkit/tree/main/01_lesson)
- 🔗 [Chapter 2 Starter Code](https://github.com/gitdagray/react_redux_toolkit/tree/main/02_lesson_starter)
- 🔗 [Chapter 2 Completed Code](https://github.com/gitdagray/react_redux_toolkit/tree/main/02_lesson)