Compare commits

..

No commits in common. "9261ffe06388d5664de93b461fbba39d491f0d2c" and "024b2b589dcd520c82b3263c4968d42bd076e992" have entirely different histories.

9 changed files with 68 additions and 206 deletions

View File

@ -3,6 +3,5 @@
"trailingComma": "es5", "trailingComma": "es5",
"tabWidth": 2, "tabWidth": 2,
"semi": true, "semi": true,
"printWidth": 80, "printWidth": 80
"arrowParens": "always"
} }

View File

@ -9,7 +9,6 @@
"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",
@ -4240,29 +4239,6 @@
"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",
@ -7267,9 +7243,9 @@
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==" "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg=="
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.2", "version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@ -12773,11 +12749,6 @@
"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",
@ -18906,28 +18877,6 @@
"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",
@ -21144,9 +21093,9 @@
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==" "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg=="
}, },
"follow-redirects": { "follow-redirects": {
"version": "1.15.2", "version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
}, },
"fork-ts-checker-webpack-plugin": { "fork-ts-checker-webpack-plugin": {
"version": "6.5.0", "version": "6.5.0",
@ -24961,11 +24910,6 @@
} }
} }
}, },
"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",

View File

@ -4,7 +4,6 @@
"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",

View File

@ -1,19 +0,0 @@
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;

View File

@ -1,44 +1,32 @@
import { useDispatch, useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { import { selectAllPosts } from './postsSlice';
fetchPosts, import PostAuthor from './PostAuthor';
getPostsError, import TimeAgo from './TimeAgo';
getPostsStatus, import ReactionButtons from './ReactionButton';
selectAllPosts,
} from './postsSlice';
import PostsExcerpt from './PostsExcerpt';
import { useEffect } from 'react';
const PostsList = () => { const PostsList = () => {
const dispatch = useDispatch();
const posts = useSelector(selectAllPosts); 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) => (
<PostsExcerpt key={post.id} post={post} /> 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>
)); ));
} else if (postStatus === 'failed') {
content = <p>{error}</p>;
}
return ( return (
<section> <section>
<h2>Posts</h2> <h2>Posts</h2>
{content} {renderedPosts}
</section> </section>
); );
}; };

View File

@ -1,43 +1,42 @@
import { createSlice, nanoid, createAsyncThunk } from '@reduxjs/toolkit'; import { createSlice, nanoid } from "@reduxjs/toolkit";
import { sub } from 'date-fns'; import { sub } from "date-fns";
import axios from 'axios';
const POSTS_URL = 'https://jsonplaceholder.typicode.com/posts'; const initialState = [
{
const initialState = { id: "1",
posts: [], title: "Learning Redux Toolkit",
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' content: "I've heard good things.",
error: null, date: sub(new Date(), { minutes: 10 }).toISOString(),
}; reactions: {
thumbsUp: 0,
export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => { wow: 0,
try { heart: 0,
const response = await axios.get(POSTS_URL); rocket: 0,
return [...response.data]; coffee: 0,
} catch (err) { },
return err.message; },
} {
}); id: "2",
title: "Slice...",
export const addNewPost = createAsyncThunk( content: "The more I say slice, the more I want pizza.",
'posts/addNewPost', date: sub(new Date(), { minutes: 5 }).toISOString(),
async (initialPost) => { reactions: {
try { thumbsUp: 0,
const response = await axios.post(POSTS_URL, initialPost); wow: 0,
return response.data; heart: 0,
} catch (err) { rocket: 0,
return err.message; coffee: 0,
} },
} },
); ];
const postsSlice = createSlice({ const postsSlice = createSlice({
name: 'posts', name: "posts",
initialState, initialState,
reducers: { reducers: {
postAdded: { postAdded: {
reducer: (state, action) => { reducer: (state, action) => {
state.posts.push(action.payload); state.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)
@ -61,46 +60,15 @@ const postsSlice = createSlice({
}, },
reactionAdded(state, action) { reactionAdded(state, action) {
const { postId, reaction } = action.payload; const { postId, reaction } = action.payload;
const existingPost = state.posts.find((post) => post.id === postId); const existingPost = state.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.posts; // first posts is meaning name of the slice export const selectAllPosts = (state) => state.posts;
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;

View File

@ -1,28 +1,14 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import { createSlice } from "@reduxjs/toolkit";
import axios from 'axios'; const initialState = [
{ id: "0", name: "Dude Lebowski" },
const USERS_URL = 'https://jsonplaceholder.typicode.com/users'; { id: "1", name: "Neil Young" },
{ 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

View File

@ -4,9 +4,6 @@ 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>