Compare commits
7 Commits
1dc07f39e5
...
02_lesson
Author | SHA1 | Date |
---|---|---|
Jason Zhu | 024b2b589d | |
Jason Zhu | 47e543f2b4 | |
Jason Zhu | 1d970d5879 | |
Jason Zhu | 3b6ee94253 | |
Jason Zhu | 75f98755b7 | |
Jason Zhu | 00fcac1145 | |
Jason Zhu | 83a42fd660 |
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true,
|
||||||
|
"printWidth": 80
|
||||||
|
}
|
|
@ -9,6 +9,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^1.8.0",
|
"@reduxjs/toolkit": "^1.8.0",
|
||||||
|
"date-fns": "^2.29.3",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "^2.8.4",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
@ -5656,6 +5657,18 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "2.29.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||||
|
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.11"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/date-fns"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
|
@ -19905,6 +19918,11 @@
|
||||||
"whatwg-url": "^8.0.0"
|
"whatwg-url": "^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"date-fns": {
|
||||||
|
"version": "2.29.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||||
|
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
|
||||||
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^1.8.0",
|
"@reduxjs/toolkit": "^1.8.0",
|
||||||
|
"date-fns": "^2.29.3",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "^2.8.4",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { configureStore } from "@reduxjs/toolkit";
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
import postsReducer from "../features/posts/postsSlice";
|
import postsReducer from "../features/posts/postsSlice";
|
||||||
|
import usersReducer from "../features/users/usersSlice";
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: { posts: postsReducer },
|
reducer: { posts: postsReducer, users: usersReducer },
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,23 +1,38 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { postAdded } from "./postsSlice";
|
import { postAdded } from "./postsSlice";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
|
import { selectAllUsers } from "../users/usersSlice";
|
||||||
|
|
||||||
const AddPostForm = () => {
|
const AddPostForm = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const [title, setTitle] = useState(""); // Temporary state for adding information in form
|
const [title, setTitle] = useState(""); // Temporary state for adding information in form
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
const dispatch = useDispatch();
|
const [userId, setUserId] = useState("");
|
||||||
|
|
||||||
|
const users = useSelector(selectAllUsers); // get users from Redux store
|
||||||
|
|
||||||
const onTitleChanged = (e) => setTitle(e.target.value);
|
const onTitleChanged = (e) => setTitle(e.target.value);
|
||||||
const onContentChanged = (e) => setContent(e.target.value);
|
const onContentChanged = (e) => setContent(e.target.value);
|
||||||
|
const onAuthorChanged = (e) => setUserId(e.target.value);
|
||||||
|
|
||||||
const onSavePostClicked = () => {
|
const onSavePostClicked = () => {
|
||||||
if (title && content) {
|
if (title && content) {
|
||||||
dispatch(postAdded(title, content));
|
dispatch(postAdded(title, content, userId));
|
||||||
setTitle("");
|
setTitle("");
|
||||||
setContent("");
|
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 (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h2>Add a New Post</h2>
|
<h2>Add a New Post</h2>
|
||||||
|
@ -30,6 +45,11 @@ const AddPostForm = () => {
|
||||||
value={title}
|
value={title}
|
||||||
onChange={onTitleChanged}
|
onChange={onTitleChanged}
|
||||||
/>
|
/>
|
||||||
|
<label htmlFor="postAuthor">Author:</label>
|
||||||
|
<select id="postAuthor" value={userId} onChange={onAuthorChanged}>
|
||||||
|
<option value=""></option>
|
||||||
|
{usersOptions}
|
||||||
|
</select>
|
||||||
<label htmlFor="postContent">Content:</label>
|
<label htmlFor="postContent">Content:</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="postContent"
|
id="postContent"
|
||||||
|
@ -37,7 +57,7 @@ const AddPostForm = () => {
|
||||||
value={content}
|
value={content}
|
||||||
onChange={onContentChanged}
|
onChange={onContentChanged}
|
||||||
/>
|
/>
|
||||||
<button type="button" onClick={onSavePostClicked}>
|
<button type="button" onClick={onSavePostClicked} disabled={!canSave}>
|
||||||
Save Post
|
Save Post
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
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;
|
|
@ -1,13 +1,25 @@
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from 'react-redux';
|
||||||
import { selectAllPosts } from "./postsSlice";
|
import { selectAllPosts } from './postsSlice';
|
||||||
|
import PostAuthor from './PostAuthor';
|
||||||
|
import TimeAgo from './TimeAgo';
|
||||||
|
import ReactionButtons from './ReactionButton';
|
||||||
|
|
||||||
const PostsList = () => {
|
const PostsList = () => {
|
||||||
const posts = useSelector(selectAllPosts);
|
const posts = useSelector(selectAllPosts);
|
||||||
|
|
||||||
const renderedPosts = posts.map((post) => (
|
const orderedPosts = posts
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => b.date.localeCompare(a.date));
|
||||||
|
|
||||||
|
const renderedPosts = orderedPosts.map((post) => (
|
||||||
<article key={post.id}>
|
<article key={post.id}>
|
||||||
<h3>{post.title}</h3>
|
<h3>{post.title}</h3>
|
||||||
<p>{post.content.substring(0, 100)}</p>
|
<p>{post.content.substring(0, 100)}</p>
|
||||||
|
<p className="postCredit">
|
||||||
|
<PostAuthor userId={post.userId} />
|
||||||
|
<TimeAgo timestamp={post.date} />
|
||||||
|
</p>
|
||||||
|
<ReactionButtons post={post} />
|
||||||
</article>
|
</article>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
@ -0,0 +1,18 @@
|
||||||
|
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}>
|
||||||
|
<i>{timeAgo}</i>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TimeAgo;
|
|
@ -1,15 +1,32 @@
|
||||||
import { createSlice, nanoid } from "@reduxjs/toolkit";
|
import { createSlice, nanoid } from "@reduxjs/toolkit";
|
||||||
|
import { sub } from "date-fns";
|
||||||
|
|
||||||
const initialState = [
|
const initialState = [
|
||||||
{
|
{
|
||||||
id: "1",
|
id: "1",
|
||||||
title: "Learning Redux Toolkit",
|
title: "Learning Redux Toolkit",
|
||||||
content: "I've heard good things.",
|
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",
|
id: "2",
|
||||||
title: "Slice...",
|
title: "Slice...",
|
||||||
content: "The more I say slice, the more I want pizza.",
|
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,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -21,21 +38,38 @@ const postsSlice = createSlice({
|
||||||
reducer: (state, action) => {
|
reducer: (state, action) => {
|
||||||
state.push(action.payload);
|
state.push(action.payload);
|
||||||
},
|
},
|
||||||
prepare(title, content) {
|
prepare(title, content, userId) {
|
||||||
|
// adding new post also need userid (author)
|
||||||
return {
|
return {
|
||||||
payload: {
|
payload: {
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
|
userId,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
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]++; // this kind of immer action can only happen in slice
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const selectAllPosts = (state) => state.posts;
|
export const selectAllPosts = (state) => state.posts;
|
||||||
|
|
||||||
export const { postAdded } = postsSlice.actions;
|
export const { postAdded, reactionAdded } = postsSlice.actions;
|
||||||
|
|
||||||
export default postsSlice.reducer;
|
export default postsSlice.reducer;
|
||||||
|
|
|
@ -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: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Selectors
|
||||||
|
export const selectAllUsers = (state) => state.users;
|
||||||
|
|
||||||
|
export default usersSlice.reducer;
|
Loading…
Reference in New Issue