Compare commits

..

7 Commits

11 changed files with 180 additions and 10 deletions

View File

@ -0,0 +1,7 @@
{
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"printWidth": 80
}

View File

@ -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",

View File

@ -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",

View File

@ -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 },
}); });

View File

@ -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>

View File

@ -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;

View File

@ -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>
)); ));

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,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}>
&nbsp; <i>{timeAgo}</i>
</span>
);
};
export default TimeAgo;

View File

@ -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;

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: {},
});
// Selectors
export const selectAllUsers = (state) => state.users;
export default usersSlice.reducer;