Added lesson 3

02_lesson
gitdagray 2022-03-31 17:56:58 -05:00
parent a405f40185
commit f10fae7298
42 changed files with 55595 additions and 0 deletions

23
03_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*

27268
03_lesson/package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
{
"name": "02_lesson",
"version": "0.1.0",
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^1.8.0",
"axios": "^0.26.1",
"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,81 @@
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { addNewPost } 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 [addRequestStatus, setAddRequestStatus] = useState('idle')
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 canSave = [title, content, userId].every(Boolean) && addRequestStatus === 'idle';
const onSavePostClicked = () => {
if (canSave) {
try {
setAddRequestStatus('pending')
dispatch(addNewPost({ title, body: content, userId })).unwrap()
setTitle('')
setContent('')
setUserId('')
} catch (err) {
console.error('Failed to save the post', err)
} finally {
setAddRequestStatus('idle')
}
}
}
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,18 @@
import PostAuthor from "./PostAuthor";
import TimeAgo from "./TimeAgo";
import ReactionButtons from "./ReactionButtons";
const PostsExcerpt = ({ post }) => {
return (
<article>
<h3>{post.title}</h3>
<p>{post.body.substring(0, 100)}</p>
<p className="postCredit">
<PostAuthor userId={post.userId} />
<TimeAgo timestamp={post.date} />
</p>
<ReactionButtons post={post} />
</article>
)
}
export default PostsExcerpt

View File

@ -0,0 +1,36 @@
import { useSelector, useDispatch } from "react-redux";
import { selectAllPosts, getPostsStatus, getPostsError, fetchPosts } from "./postsSlice";
import { useEffect } from "react";
import PostsExcerpt from "./PostsExcerpt";
const PostsList = () => {
const dispatch = useDispatch();
const posts = useSelector(selectAllPosts);
const postStatus = useSelector(getPostsStatus);
const error = useSelector(getPostsError);
useEffect(() => {
if (postStatus === 'idle') {
dispatch(fetchPosts())
}
}, [postStatus, dispatch])
let content;
if (postStatus === 'loading') {
content = <p>"Loading..."</p>;
} else if (postStatus === 'succeeded') {
const orderedPosts = posts.slice().sort((a, b) => b.date.localeCompare(a.date))
content = orderedPosts.map(post => <PostsExcerpt key={post.id} post={post} />)
} else if (postStatus === 'failed') {
content = <p>{error}</p>;
}
return (
<section>
<h2>Posts</h2>
{content}
</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,116 @@
import { createSlice, nanoid, createAsyncThunk } from "@reduxjs/toolkit";
import { sub } from 'date-fns';
import axios from "axios";
const POSTS_URL = 'https://jsonplaceholder.typicode.com/posts';
const initialState = {
posts: [],
status: 'idle', //'idle' | 'loading' | 'succeeded' | 'failed'
error: null
}
export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
try {
const response = await axios.get(POSTS_URL)
return [...response.data];
} catch (err) {
return err.message;
}
})
export const addNewPost = createAsyncThunk('posts/addNewPost', async (initialPost) => {
try {
const response = await axios.post(POSTS_URL, initialPost)
return response.data
} catch (err) {
return err.message;
}
})
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
postAdded: {
reducer(state, action) {
state.posts.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.posts.find(post => post.id === postId)
if (existingPost) {
existingPost.reactions[reaction]++
}
}
},
extraReducers(builder) {
builder
.addCase(fetchPosts.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded'
// Adding date and reactions
let min = 1;
const loadedPosts = action.payload.map(post => {
post.date = sub(new Date(), { minutes: min++ }).toISOString();
post.reactions = {
thumbsUp: 0,
wow: 0,
heart: 0,
rocket: 0,
coffee: 0
}
return post;
});
// Add any fetched posts to the array
state.posts = state.posts.concat(loadedPosts)
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
.addCase(addNewPost.fulfilled, (state, action) => {
action.payload.userId = Number(action.payload.userId)
action.payload.date = new Date().toISOString();
action.payload.reactions = {
thumbsUp: 0,
hooray: 0,
heart: 0,
rocket: 0,
eyes: 0
}
console.log(action.payload)
state.posts.push(action.payload)
})
}
})
export const selectAllPosts = (state) => state.posts.posts;
export const getPostsStatus = (state) => state.posts.status;
export const getPostsError = (state) => state.posts.error;
export const { postAdded, reactionAdded } = postsSlice.actions
export default postsSlice.reducer

View File

@ -0,0 +1,30 @@
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
const USERS_URL = 'https://jsonplaceholder.typicode.com/users';
const initialState = []
export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
try {
const response = await axios.get(USERS_URL);
return [...response.data];
} catch (err) {
return err.message;
}
})
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers(builder) {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
return action.payload;
})
}
})
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,18 @@
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';
import { fetchUsers } from './features/users/usersSlice';
store.dispatch(fetchUsers());
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);

23
03_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*

27251
03_lesson_starter/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')
);

View File

@ -58,5 +58,7 @@
- 🔗 [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)
- 🔗 [Chapter 3 Starter Code](https://github.com/gitdagray/react_redux_toolkit/tree/main/03_lesson_starter)
- 🔗 [Chapter 3 Completed Code](https://github.com/gitdagray/react_redux_toolkit/tree/main/03_lesson)