Compare commits
6 Commits
024b2b589d
...
9261ffe063
Author | SHA1 | Date |
---|---|---|
jason-zhu | 9261ffe063 | |
Jason Zhu | 14c4682dcf | |
Jason Zhu | e8dedad084 | |
Jason Zhu | 2339529dfd | |
Jason Zhu | f0d02e6e02 | |
Jason Zhu | 476e7259e7 |
|
@ -3,5 +3,6 @@
|
||||||
"trailingComma": "es5",
|
"trailingComma": "es5",
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"printWidth": 80
|
"printWidth": 80,
|
||||||
|
"arrowParens": "always"
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^1.8.0",
|
"@reduxjs/toolkit": "^1.8.0",
|
||||||
|
"axios": "^1.3.4",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "^2.8.4",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
@ -4239,6 +4240,29 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/axios/node_modules/form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/axobject-query": {
|
"node_modules/axobject-query": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
||||||
|
@ -7243,9 +7267,9 @@
|
||||||
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg=="
|
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg=="
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.14.9",
|
"version": "1.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||||
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
|
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
|
@ -12749,6 +12773,11 @@
|
||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"node_modules/psl": {
|
"node_modules/psl": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
|
||||||
|
@ -18877,6 +18906,28 @@
|
||||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.4.1.tgz",
|
||||||
"integrity": "sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw=="
|
"integrity": "sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw=="
|
||||||
},
|
},
|
||||||
|
"axios": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
|
||||||
|
"requires": {
|
||||||
|
"follow-redirects": "^1.15.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"requires": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"axobject-query": {
|
"axobject-query": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
||||||
|
@ -21093,9 +21144,9 @@
|
||||||
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg=="
|
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg=="
|
||||||
},
|
},
|
||||||
"follow-redirects": {
|
"follow-redirects": {
|
||||||
"version": "1.14.9",
|
"version": "1.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||||
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
|
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
|
||||||
},
|
},
|
||||||
"fork-ts-checker-webpack-plugin": {
|
"fork-ts-checker-webpack-plugin": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
|
@ -24910,6 +24961,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"psl": {
|
"psl": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^1.8.0",
|
"@reduxjs/toolkit": "^1.8.0",
|
||||||
|
"axios": "^1.3.4",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "^2.8.4",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import ReactionButtons from './ReactionButtons';
|
||||||
|
import PostAuthor from './PostAuthor';
|
||||||
|
import TimeAgo from './TimeAgo';
|
||||||
|
|
||||||
|
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;
|
|
@ -1,32 +1,44 @@
|
||||||
import { useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { selectAllPosts } from './postsSlice';
|
import {
|
||||||
import PostAuthor from './PostAuthor';
|
fetchPosts,
|
||||||
import TimeAgo from './TimeAgo';
|
getPostsError,
|
||||||
import ReactionButtons from './ReactionButton';
|
getPostsStatus,
|
||||||
|
selectAllPosts,
|
||||||
|
} from './postsSlice';
|
||||||
|
import PostsExcerpt from './PostsExcerpt';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
const PostsList = () => {
|
const PostsList = () => {
|
||||||
const posts = useSelector(selectAllPosts);
|
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
|
const orderedPosts = posts
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => b.date.localeCompare(a.date));
|
.sort((a, b) => b.date.localeCompare(a.date));
|
||||||
|
content = orderedPosts.map((post) => (
|
||||||
const renderedPosts = orderedPosts.map((post) => (
|
<PostsExcerpt key={post.id} post={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>
|
|
||||||
));
|
));
|
||||||
|
} else if (postStatus === 'failed') {
|
||||||
|
content = <p>{error}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h2>Posts</h2>
|
<h2>Posts</h2>
|
||||||
{renderedPosts}
|
{content}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,42 +1,43 @@
|
||||||
import { createSlice, nanoid } from "@reduxjs/toolkit";
|
import { createSlice, nanoid, createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { sub } from "date-fns";
|
import { sub } from 'date-fns';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
const initialState = [
|
const POSTS_URL = 'https://jsonplaceholder.typicode.com/posts';
|
||||||
{
|
|
||||||
id: "1",
|
const initialState = {
|
||||||
title: "Learning Redux Toolkit",
|
posts: [],
|
||||||
content: "I've heard good things.",
|
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||||
date: sub(new Date(), { minutes: 10 }).toISOString(),
|
error: null,
|
||||||
reactions: {
|
};
|
||||||
thumbsUp: 0,
|
|
||||||
wow: 0,
|
export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
|
||||||
heart: 0,
|
try {
|
||||||
rocket: 0,
|
const response = await axios.get(POSTS_URL);
|
||||||
coffee: 0,
|
return [...response.data];
|
||||||
},
|
} catch (err) {
|
||||||
},
|
return err.message;
|
||||||
{
|
}
|
||||||
id: "2",
|
});
|
||||||
title: "Slice...",
|
|
||||||
content: "The more I say slice, the more I want pizza.",
|
export const addNewPost = createAsyncThunk(
|
||||||
date: sub(new Date(), { minutes: 5 }).toISOString(),
|
'posts/addNewPost',
|
||||||
reactions: {
|
async (initialPost) => {
|
||||||
thumbsUp: 0,
|
try {
|
||||||
wow: 0,
|
const response = await axios.post(POSTS_URL, initialPost);
|
||||||
heart: 0,
|
return response.data;
|
||||||
rocket: 0,
|
} catch (err) {
|
||||||
coffee: 0,
|
return err.message;
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
];
|
);
|
||||||
|
|
||||||
const postsSlice = createSlice({
|
const postsSlice = createSlice({
|
||||||
name: "posts",
|
name: 'posts',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
postAdded: {
|
postAdded: {
|
||||||
reducer: (state, action) => {
|
reducer: (state, action) => {
|
||||||
state.push(action.payload);
|
state.posts.push(action.payload);
|
||||||
},
|
},
|
||||||
prepare(title, content, userId) {
|
prepare(title, content, userId) {
|
||||||
// adding new post also need userid (author)
|
// adding new post also need userid (author)
|
||||||
|
@ -60,15 +61,46 @@ const postsSlice = createSlice({
|
||||||
},
|
},
|
||||||
reactionAdded(state, action) {
|
reactionAdded(state, action) {
|
||||||
const { postId, reaction } = action.payload;
|
const { postId, reaction } = action.payload;
|
||||||
const existingPost = state.find((post) => post.id === postId);
|
const existingPost = state.posts.find((post) => post.id === postId);
|
||||||
if (existingPost) {
|
if (existingPost) {
|
||||||
existingPost.reactions[reaction]++; // this kind of immer action can only happen in slice
|
existingPost.reactions[reaction]++; // this kind of immer action can only happen in slice
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
extraReducers(builder) {
|
||||||
|
// use traditional redux way of defining reducers
|
||||||
|
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,
|
||||||
|
horray: 0,
|
||||||
|
heart: 0,
|
||||||
|
rocket: 0,
|
||||||
|
eyes: 0,
|
||||||
|
};
|
||||||
|
return post;
|
||||||
|
});
|
||||||
|
// Add an fetched posts to the array
|
||||||
|
state.posts = state.posts.concat(loadedPosts);
|
||||||
|
})
|
||||||
|
.addCase(fetchPosts.rejected, (state, action) => {
|
||||||
|
state.status = 'failed';
|
||||||
|
state.error = action.error.message;
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const selectAllPosts = (state) => state.posts;
|
export const selectAllPosts = (state) => state.posts.posts; // first posts is meaning name of the slice
|
||||||
|
export const getPostsStatus = (state) => state.posts.status;
|
||||||
|
export const getPostsError = (state) => state.posts.error;
|
||||||
|
|
||||||
export const { postAdded, reactionAdded } = postsSlice.actions;
|
export const { postAdded, reactionAdded } = postsSlice.actions;
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,28 @@
|
||||||
import { createSlice } from "@reduxjs/toolkit";
|
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
const initialState = [
|
import axios from 'axios';
|
||||||
{ id: "0", name: "Dude Lebowski" },
|
|
||||||
{ id: "1", name: "Neil Young" },
|
const USERS_URL = 'https://jsonplaceholder.typicode.com/users';
|
||||||
{ id: "2", name: "Dave Gray" },
|
|
||||||
];
|
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({
|
const usersSlice = createSlice({
|
||||||
name: "users",
|
name: 'users',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {},
|
reducers: {},
|
||||||
|
extraReducers(builder) {
|
||||||
|
builder.addCase(fetchUsers.fulfilled, (state, action) => {
|
||||||
|
return action.payload;
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Selectors
|
// Selectors
|
||||||
|
|
|
@ -4,6 +4,9 @@ import './index.css';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { store } from './app/store';
|
import { store } from './app/store';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
import { fetchUsers } from './features/users/usersSlice';
|
||||||
|
|
||||||
|
store.dispatch(fetchUsers()); // Fetch users before rendering page
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
|
Loading…
Reference in New Issue